@clicktap/ui 2.1.4 → 2.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.
@@ -1,4 +1,4 @@
1
1
  import { PinInputProps } from './PinInput.types';
2
2
  /** based on https://github.com/chakra-ui/chakra-ui/blob/main/packages/components/src/pin-input/use-pin-input.ts */
3
- export declare function PinInput({ autoFocus, description, errorMessage, isDisabled, isInvalid, isMasked, isRequired, label, length, name: _name, onChange: controlledOnChange, value: _value, type, validationBehavior: _validationBehavior, className, classNames, inputProps, ...props }: PinInputProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare function PinInput({ autoFocus, description, errorMessage, isDisabled, isInvalid, isMasked, isRequired, label, length, name, onChange: controlledOnChange, value, type, validationBehavior: _validationBehavior, className, classNames, inputProps, ...props }: PinInputProps): import("react/jsx-runtime").JSX.Element;
4
4
  export default PinInput;
@@ -1 +1 @@
1
- "use client";import{jsxs as $,jsx as x}from"react/jsx-runtime";import{useState as L,useRef as R,useEffect as j,useCallback as V}from"react";import{Group as T,Label as _}from"react-aria-components";import{cn as f}from"../../utils/cn.js";function C({autoFocus:y=!1,description:D,errorMessage:b,isDisabled:I=!1,isInvalid:g=!1,isMasked:N=!1,isRequired:z=!0,label:k,length:i=6,name:P,onChange:d,value:S="",type:w="numeric",validationBehavior:W,className:B,classNames:p,inputProps:A,...J}){const[r,h]=L(()=>Array(i).fill("")),o=R([]);j(()=>{o.current=o.current.slice(0,i)},[i]),j(()=>{y&&o.current[0]?.focus()},[y]);const K=V(t=>{const e=t.target.value,u=Number(t.target.getAttribute("data-pin-input-index"));e!==""&&u<i-1&&o.current?.[u+1]?.focus();const l=r.map((s,c)=>c===u?e:s);h(l);const a=l.join("");d?.(a)},[r,i,d]),v=V(t=>{const e=Number(t.currentTarget.getAttribute("data-pin-input-index"));if(t.ctrlKey||t.metaKey)return;const u=["Backspace","Delete","ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Control","Alt","Meta","Shift","Tab","Enter","Escape"],l={alpha:/^[a-z]$/i,alphanumeric:/^[a-z0-9]$/i,numeric:/^[0-9]$/i};if(!t.key.match(l[w])&&!u.includes(t.key)&&t.preventDefault(),r[e]===""&&e>0&&t.key==="Backspace"){const a=r.map((c,m)=>m===e-1?"":c);h(a),o.current?.[e-1]?.focus(),t.preventDefault();const s=a.join("");d?.(s)}if(r[e]===""&&e<r.length-1&&t.key==="Delete"){const a=r.map((c,m)=>m===e+1?"":c);h(a),o.current?.[e+1]?.focus(),t.preventDefault();const s=a.join("");d?.(s)}},[r,w,d]),E=V(t=>{const e=Number(t.currentTarget.getAttribute("data-pin-input-index"));t.preventDefault();const u=t.clipboardData?.getData("text");if(!u)return;const l=u.split("").filter(n=>{switch(w){case"alpha":return/^[a-zA-Z]$/.test(n);case"alphanumeric":return/^[a-z0-9]$/i.test(n);default:return/^[0-9]$/.test(n)}});if(l.length===0)return;const a=[...r];let s=e;for(let n=0;n<l.length&&e+n<i;n++)a[e+n]=l[n],s=e+n;h(a);const c=a.join("");d?.(c);const m=s+1<i?s+1:i-1;o.current[m]?.focus()},[r,w,i,d]);return $(T,{className:f("flex flex-wrap gap-2",B),"aria-label":k,...J,children:[k&&x(_,{className:f("flex text-fg-muted text-sm grow shrink-0 basis-full",p?.label),children:k}),r.map((t,e)=>x("div",{className:f("flex flex-col w-full flex-1 text-fg",p?.inputWrap),children:x("input",{type:N?"password":"text",autoComplete:"one-time-code","data-1p-ignore":"true","data-lpignore":"true","data-bwignore":"true","data-form-type":"other",...A,className:f("border-solid border rounded-md","text-sm text-fg placeholder-fg-subtle text-center","h-10 px-2 py-0 m-0 w-full","bg-white","[&::-ms-reveal]:hidden [&::-ms-clear]:hidden","transition-all duration-200 ease-in-out","hover:border-border-emphasis","focus:outline-2 focus:outline focus:outline-border-subtle focus:border-border-emphasis","disabled:border-border-subtle disabled:bg-surface-subtle",g&&"border-danger-soft bg-danger-surface text-danger",g&&"hover:border-danger-soft focus:border-danger-soft focus:outline-border-subtle",p?.input,A?.className),onChange:K,onKeyDown:v,onPaste:E,ref:u=>{o.current[e]=u},value:t,maxLength:1,"data-pin-input-index":e,disabled:I,required:z,"aria-label":`Pin Input Digit ${e+1}`})},`pin-input-${e}`)),(D||g&&b)&&$("div",{className:f("flex flex-row flex-wrap grow shrink-0 basis-full",p?.textWrap),children:[D&&x("p",{className:f("flex text-fg-muted text-sm grow shrink-0 basis-full",p?.description),children:D}),g&&b&&typeof b=="string"&&x("p",{className:f("flex text-danger text-sm grow shrink-0 basis-full",p?.error),children:b})]})]})}export{C as PinInput,C as default};
1
+ "use client";import{jsxs as L,jsx as m}from"react/jsx-runtime";import{useState as G,useRef as H,useCallback as b,useEffect as $}from"react";import{Group as U,Label as J}from"react-aria-components";import{cn as u}from"../../utils/cn.js";const j={alpha:/^[a-z]$/i,alphanumeric:/^[a-z0-9]$/i,numeric:/^[0-9]$/};function E(g,x){const c=g.split("");return Array.from({length:x},(k,f)=>c[f]??"")}function Z({autoFocus:g=!1,description:x,errorMessage:c,isDisabled:k=!1,isInvalid:f=!1,isMasked:I=!1,isRequired:K=!0,label:D,length:o=6,name:y,onChange:C,value:A,type:d="numeric",validationBehavior:M,className:R,classNames:p,inputProps:N,...W}){const v=A!==void 0,[T,_]=G(()=>E(A??"",o)),r=v?E(A,o):T,s=H([]),l=b(e=>{_(e),C?.(e.join(""))},[C]),h=b(e=>j[d].test(e),[d]),w=b((e,t)=>{const n=[...r];let a=e;for(let i=0;i<t.length&&e+i<o;i++)n[e+i]=t[i],a=e+i;l(n),s.current[Math.min(a+1,o-1)]?.focus()},[r,o,l]);$(()=>{s.current=s.current.slice(0,o)},[o]),$(()=>{g&&s.current[0]?.focus()},[g]);const z=b(e=>{const t=e.target.value,n=Number(e.target.getAttribute("data-pin-input-index"));if(t===""){l(r.map((V,q)=>q===n?"":V));return}const a=r[n],B=(a!==""&&t.startsWith(a)?t.slice(a.length):t).split("").filter(h);B.length!==0&&w(n,B)},[r,l,h,w]),P=b(e=>{const t=Number(e.currentTarget.getAttribute("data-pin-input-index"));if(e.ctrlKey||e.metaKey)return;const n=["Backspace","Delete","ArrowLeft","ArrowRight","ArrowUp","ArrowDown","Control","Alt","Meta","Shift","Tab","Enter","Escape"];!j[d].test(e.key)&&!n.includes(e.key)&&e.preventDefault(),r[t]===""&&t>0&&e.key==="Backspace"&&(l(r.map((a,i)=>i===t-1?"":a)),s.current?.[t-1]?.focus(),e.preventDefault()),r[t]===""&&t<r.length-1&&e.key==="Delete"&&(l(r.map((a,i)=>i===t+1?"":a)),s.current?.[t+1]?.focus(),e.preventDefault())},[r,d,l]),S=b(e=>{const t=Number(e.currentTarget.getAttribute("data-pin-input-index"));e.preventDefault();const n=e.clipboardData?.getData("text");if(!n)return;const a=n.split("").filter(h);a.length!==0&&w(t,a)},[h,w]);return L(U,{className:u("flex flex-wrap gap-2",R),"aria-label":D,...W,children:[D&&m(J,{className:u("flex text-fg-muted text-sm grow shrink-0 basis-full",p?.label),children:D}),r.map((e,t)=>m("div",{className:u("flex flex-col w-full flex-1 text-fg",p?.inputWrap),children:m("input",{type:I?"password":"text",autoComplete:t===0?"one-time-code":"off",inputMode:d==="numeric"?"numeric":"text",pattern:d==="numeric"?"[0-9]*":void 0,"data-1p-ignore":"true","data-lpignore":"true","data-bwignore":"true","data-form-type":"other",...N,className:u("border-solid border rounded-md","text-sm text-fg placeholder-fg-subtle text-center","h-10 px-2 py-0 m-0 w-full","bg-white","[&::-ms-reveal]:hidden [&::-ms-clear]:hidden","transition-all duration-200 ease-in-out","hover:border-border-emphasis","focus:outline-2 focus:outline focus:outline-border-subtle focus:border-border-emphasis","disabled:border-border-subtle disabled:bg-surface-subtle",f&&"border-danger-soft bg-danger-surface text-danger",f&&"hover:border-danger-soft focus:border-danger-soft focus:outline-border-subtle",p?.input,N?.className),onChange:z,onKeyDown:P,onPaste:S,ref:n=>{s.current[t]=n},value:e,maxLength:void 0,"data-pin-input-index":t,disabled:k,required:K,"aria-label":`Pin Input Digit ${t+1}`})},`pin-input-${t}`)),y&&m("input",{type:"hidden",name:y,value:r.join("")}),(x||f&&c)&&L("div",{className:u("flex flex-row flex-wrap grow shrink-0 basis-full",p?.textWrap),children:[x&&m("p",{className:u("flex text-fg-muted text-sm grow shrink-0 basis-full",p?.description),children:x}),f&&c&&typeof c=="string"&&m("p",{className:u("flex text-danger text-sm grow shrink-0 basis-full",p?.error),children:c})]})]})}export{Z as PinInput,Z as default};
@@ -6,7 +6,7 @@ import { SlotsToClasses } from '../../types/SlotsToClasses';
6
6
  * `inputProps` — they are the component's wiring, and overriding them breaks it.
7
7
  */
8
8
  type OwnedInputProps = 'value' | 'onChange' | 'onKeyDown' | 'onPaste' | 'ref' | 'maxLength' | 'disabled' | 'required';
9
- /** @todo extend certain textfield props like name, validationBehavior and isRequired */
9
+ /** @todo extend certain textfield props like validationBehavior and isRequired */
10
10
  export interface PinInputProps extends GroupProps {
11
11
  /**
12
12
  * Extra props for each digit `<input>` — the escape hatch for anything the component does not
@@ -22,8 +22,13 @@ export interface PinInputProps extends GroupProps {
22
22
  errorMessage?: string | ((validation: ValidationResult) => string);
23
23
  label?: string;
24
24
  length?: number;
25
+ /** Rendered as a hidden input carrying the joined code, so a native form submit sees it. */
25
26
  name?: string;
26
27
  onChange?: (...event: any[]) => void;
28
+ /**
29
+ * The joined code. When given, the component is controlled: the boxes are derived from it, so a
30
+ * form's reset()/setValue('') clears them (CTAP-1749). Omit it for an uncontrolled input.
31
+ */
27
32
  value?: string;
28
33
  isMasked?: boolean;
29
34
  isRequired?: boolean;
@@ -0,0 +1,52 @@
1
+ # `createAuthAwareGraphqlClient`: a server-side request-headers provider, so SSR calls can carry the shopper
2
+
3
+ **Date:** 2026-09-15
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** `utils/createAuthAwareGraphqlClient` — new export `setServerRequestHeadersProvider()`; `executeGraphql` and `createAuthAwareFetch` consult it server-side
6
+ **Classification:** FEATURE
7
+ **Breaking:** No. Nothing changes until a provider is registered; explicit request headers always win over provided ones.
8
+ **Ticket:** CTAP-1719
9
+
10
+ ## Why
11
+
12
+ A request the Node server makes to the middleware on a shopper's behalf (SSR, route handlers)
13
+ carried nothing identifying the shopper — the middleware saw the Node container for every visitor.
14
+ Anything keyed on the client there (a rate limit, a lockout) would put the whole storefront in one
15
+ bucket.
16
+
17
+ The fix has to be in the shared client, because that is the one place every server-side request
18
+ passes through — `graphqlRequest`, the quote/checkout transports (`request` and `createFetch`), route
19
+ handlers. But this module is framework-agnostic and reachable from client components, so it cannot
20
+ import `next/headers` itself. Hence a **provider** the app registers:
21
+
22
+ ```ts
23
+ import { setServerRequestHeadersProvider } from '@clicktap/ui/utils/createAuthAwareGraphqlClient';
24
+
25
+ setServerRequestHeadersProvider(() => {
26
+ const forwardedFor = headers().get('x-forwarded-for'); // next/headers, in a server-only module
27
+ return forwardedFor ? { 'x-forwarded-for': forwardedFor } : undefined;
28
+ });
29
+ ```
30
+
31
+ ## Rules
32
+
33
+ - Consulted **only server-side** (`typeof window === 'undefined'`); the browser path is untouched.
34
+ - **Explicit headers win.** A header the caller passed is never overwritten by the provider.
35
+ - **A throwing provider contributes nothing.** `headers()` throws outside a request scope (a static
36
+ render, a warm-up); that is "nothing to forward", not a failed request.
37
+ - Sync or async providers are accepted.
38
+ - Applies to both `createAuthAwareGraphqlClient()` requests and `createAuthAwareFetch()` calls.
39
+
40
+ ## What a consumer does
41
+
42
+ Register a provider from a **server-only** module (`import 'server-only'` + `next/headers`), imported
43
+ by the root layouts and by any route handler that fetches — see the `frontend` guide of the same
44
+ date for the reference module. Forward the incoming `X-Forwarded-For` **verbatim**: the middleware's
45
+ trusted-proxy walk reads it from its own end, so the browser's forged entries are inert and
46
+ "cleaning" the chain here would only remove information.
47
+
48
+ ## Verification
49
+
50
+ ```bash
51
+ nx run ui:test -- createAuthAwareGraphqlClient
52
+ ```
@@ -0,0 +1,57 @@
1
+ # `PinInput`: Android autofill fills every box, mobile keyboards open numeric, and `value` is a real prop
2
+
3
+ **Date:** 2026-09-18
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** `src/components/PinInput/PinInput.tsx`, `PinInput.types.ts`. Every consumer of `<PinInput>` (signup, forgot-password and email/password-change verification on the storefront).
6
+ **Classification:** BUGFIX (three) + IMPROVEMENT (`value`/`name` honored)
7
+ **Breaking:** No API change. **Behavioral:** passing `value` now controls the boxes (it was silently ignored); `maxLength` is no longer rendered; `autoComplete="one-time-code"` is on the first box only.
8
+ **Ticket:** CTAP-1749 (from NOCOE-2944)
9
+
10
+ ## What was wrong
11
+
12
+ Four things, all in this component (a consumer wrapper such as noco's `libs/noco-ui` only restyles it):
13
+
14
+ 1. **Android autofill and Gboard's clipboard chip lost five of six digits.** The paste handler split
15
+ digits across the boxes correctly, but autofill and the keyboard chip insert text as an *input*
16
+ event, not a paste event, so they went through the plain change handler, where `maxLength={1}`
17
+ kept only the first character. Desktop Ctrl+V worked, which is why it looked fixed.
18
+ 2. **No `inputMode`.** A numeric code opened the full keyboard on phones.
19
+ 3. **`autoComplete="one-time-code"` on all six boxes.** iOS offers the SMS code to the field that
20
+ advertises it; six advertising boxes offered the whole code to whichever one had focus.
21
+ 4. **`value` and `name` were accepted and discarded.** The component kept its own state, so
22
+ react-hook-form's `reset()` / `setValue('')` after a wrong code left six stale digits in place.
23
+
24
+ ## What changed
25
+
26
+ - The change handler now distributes a multi-character value from the box that received it, the
27
+ way the paste handler always did: filtered to the code's alphabet, one character per box, extras
28
+ dropped, focus on the box after the last one written, **one** `onChange` with the joined code.
29
+ `maxLength` is gone (it was the truncation) and is pinned to `undefined` after `inputProps` so a
30
+ consumer cannot put it back.
31
+ - `inputMode="numeric"` and `pattern="[0-9]*"` when `type` is numeric; `inputMode="text"` otherwise.
32
+ - `autoComplete="one-time-code"` on the first box, `off` on the rest. The password-manager opt-outs
33
+ stay on every box.
34
+ - `value`, when given, controls the boxes: they are derived from it, so an empty value clears them.
35
+ Omit it for an uncontrolled input, as before. `name` renders a hidden input carrying the joined
36
+ code, so a native form submit sees it.
37
+
38
+ ## ⚠️ Consumer impact
39
+
40
+ - **If you pass `value`, it now means something.** Every storefront usage passes
41
+ `value={value ?? ''}` from a `Controller`, which is exactly the controlled shape: typing calls
42
+ `onChange`, the form stores the code, the boxes re-derive from it. A consumer that passes a stale
43
+ or constant `value` (say, `value=""` without wiring `onChange` into state) will find the boxes
44
+ never fill. Wire `onChange` to whatever produces `value`, or drop `value` for uncontrolled use.
45
+ - A consumer that overrode `autoComplete` through `inputProps` still wins: `inputProps` is applied
46
+ after the defaults.
47
+ - Nothing to change for the four storefront forms; they get all four fixes by taking the release.
48
+
49
+ ## Verification
50
+
51
+ `PinInput.test.tsx` covers each: a multi-character change event on box 1 fills all six and reports
52
+ the code once; distribution from a middle box drops the overflow; an autofilled sentence keeps only
53
+ the digits; `inputmode`/`pattern` per type; `value=""` clears; the hidden `name` input carries the
54
+ joined code; a single typed digit still advances. All were run against the previous component and
55
+ failed before the change. On a device: Android Chrome with Gboard, tap the clipboard chip on the
56
+ code, all six boxes fill; iOS Messages offers the code on the first box; a wrong code followed by
57
+ the form's reset empties the boxes.
@@ -1,4 +1,14 @@
1
1
  [
2
+ {
3
+ "path": "2026/09/ui-2026-09-18-pin-input-autofill-and-controlled-value.md",
4
+ "module": "libs/frontend-modules/ui",
5
+ "package": "@clicktap/ui",
6
+ "date": "2026-09-18",
7
+ "title": "`PinInput`: Android autofill fills every box, mobile keyboards open numeric, and `value` is a real prop",
8
+ "classification": "BUGFIX (three) + IMPROVEMENT (`value`/`name` honored)",
9
+ "breaking": "No API change. **Behavioral:** passing `value` now controls the boxes (it was silently ignored); `maxLength` is no longer rendered; `autoComplete=\"one-time-code\"` is on the first box only.",
10
+ "affects": "`src/components/PinInput/PinInput.tsx`, `PinInput.types.ts`. Every consumer of `<PinInput>` (signup, forgot-password and email/password-change verification on the storefront)."
11
+ },
2
12
  {
3
13
  "path": "2026/09/ui-2026-09-16-modal-overlay-detached-motion-element.md",
4
14
  "module": "libs/frontend-modules/ui",
@@ -9,6 +19,16 @@
9
19
  "breaking": "No API change. **Behavioral:** while its trigger is closed and has never opened, the overlay now renders `null` instead of a mounted `motion()` wrapper around an empty react-aria overlay. Nothing consumer-visible was in the DOM before either.",
10
20
  "affects": "`src/components/ModalOverlay/ModalOverlay.tsx` — every trigger-driven `<ModalOverlay>` (no `isOpen` prop, state from `DialogTrigger` / `OverlayTriggerStateContext`). Controlled overlays (`isOpen` given) are unchanged."
11
21
  },
22
+ {
23
+ "path": "2026/09/ui-2026-09-15-server-request-headers-provider.md",
24
+ "module": "libs/frontend-modules/ui",
25
+ "package": "@clicktap/ui",
26
+ "date": "2026-09-15",
27
+ "title": "`createAuthAwareGraphqlClient`: a server-side request-headers provider, so SSR calls can carry the shopper",
28
+ "classification": "FEATURE",
29
+ "breaking": "No. Nothing changes until a provider is registered; explicit request headers always win over provided ones.",
30
+ "affects": "`utils/createAuthAwareGraphqlClient` — new export `setServerRequestHeadersProvider()`; `executeGraphql` and `createAuthAwareFetch` consult it server-side"
31
+ },
12
32
  {
13
33
  "path": "2026/09/ui-2026-09-09-graphql-field-errors-reported-in-production.md",
14
34
  "module": "libs/frontend-modules/ui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "2.1.4",
3
+ "version": "2.3.0",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",
@@ -111,6 +111,24 @@ declare class AuthAwareEventBus {
111
111
  * listen, regardless of which call triggered the event).
112
112
  */
113
113
  export declare const authAwareEvents: AuthAwareEventBus;
114
+ /**
115
+ * Headers a SERVER-SIDE caller wants attached to every outbound request, resolved per request.
116
+ *
117
+ * CTAP-1719: a request the Node server makes to the middleware on a shopper's behalf (SSR, route
118
+ * handlers) carried no trace of the shopper — the middleware saw the Node container's address for
119
+ * every visitor, so anything keyed on the client (a rate limit, a lockout) would put the whole
120
+ * storefront in one bucket. The app registers a provider that reads the incoming request's
121
+ * `X-Forwarded-For` (via `next/headers`, which only exists in a request scope) and returns it here,
122
+ * so the middleware's trusted-proxy walk lands on the browser rather than on Node.
123
+ *
124
+ * Registered rather than imported because this module must stay framework-agnostic and bundleable
125
+ * on the client: `next/headers` cannot be imported from anything a client component reaches. The
126
+ * provider is only ever consulted server-side (`typeof window === 'undefined'`); explicit request
127
+ * headers always win over what it returns; and a provider that throws (no request scope — a build-time
128
+ * render, a cron) contributes nothing rather than failing the request.
129
+ */
130
+ export type ServerRequestHeadersProvider = () => HeadersInit | undefined | Promise<HeadersInit | undefined>;
131
+ export declare function setServerRequestHeadersProvider(provider: ServerRequestHeadersProvider | undefined): void;
114
132
  export type AuthAwareGraphqlRequest = <TData, TVariables extends GraphqlVariables = GraphqlVariables>(url: string, document: GraphqlDocument<TData, TVariables>, variables?: TVariables, requestHeaders?: HeadersInit) => Promise<TData>;
115
133
  export type AuthAwareGraphqlRequestWithOptions = <TData, TVariables extends GraphqlVariables = GraphqlVariables>(options: {
116
134
  url: string;
@@ -1 +1 @@
1
- class l extends Error{constructor(t,s){const e=t.errors?.[0]?.message;super(`GraphQL request failed: ${e??`HTTP ${t.status}`}`),this.name="ClientError",this.response=t,this.request=s}}class f{constructor(){this.listeners=new Map}subscribe(t,s){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(s),()=>{this.listeners.get(t)?.delete(s)}}dispatch(t,s){this.listeners.get(t)?.forEach(e=>e(s))}}const u=new f;function p(r,t){r===401?u.dispatch("unauthenticated",t):r===403?u.dispatch("permissionDenied",t):r>=500&&u.dispatch("serverError",t)}function w(r,t,s){if(!s.length)return;const e=r.match(/^\s*(?:query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/)?.[1];for(const n of s){const c=n.path?.join(".")??"unknown",a=`[GraphQL] ${e??"operation"} — field error at ${c}: ${n.message}`;if(process.env.NODE_ENV==="development"){console.error(a,{error:n,request:{operationName:e,variables:t,query:r}});continue}console.error(a,{operationName:e,path:n.path,code:n.extensions?.code,debugMessage:n.extensions?.debugMessage})}}async function d(r){const{url:t,query:s,variables:e,requestHeaders:n,signal:c}=r,a=new Headers(n);a.set("Content-Type","application/json"),a.has("Accept")||a.set("Accept","application/json");const i=await fetch(t,{method:"POST",headers:a,body:JSON.stringify({query:s,variables:e}),signal:c});let o;try{o=await i.json()}catch{o=void 0}if(!i.ok){const h=new l({status:i.status,data:o?.data,errors:o?.errors},{query:s,variables:e});throw p(i.status,{status:i.status,error:h}),h}if(o?.data)return o.errors?.length&&w(s,e,o.errors),o.data;throw new l({status:i.status,errors:o?.errors},{query:s,variables:e})}function g(){const r=async(e,n,c,a)=>d({url:e,query:String(n),variables:c,requestHeaders:a}),t=async e=>d({url:e.url,query:String(e.document),variables:e.variables,requestHeaders:e.requestHeaders,signal:e.signal}),s=r;return s.withOptions=t,s}function y(){return async function(t,s){const e=await fetch(t,s);return(e.status===401||e.status===403||e.status>=500)&&p(e.status,{status:e.status,response:e.clone()}),e}}export{l as ClientError,u as authAwareEvents,y as createAuthAwareFetch,g as createAuthAwareGraphqlClient};
1
+ class l extends Error{constructor(t,s){const e=t.errors?.[0]?.message;super(`GraphQL request failed: ${e??`HTTP ${t.status}`}`),this.name="ClientError",this.response=t,this.request=s}}class v{constructor(){this.listeners=new Map}subscribe(t,s){return this.listeners.has(t)||this.listeners.set(t,new Set),this.listeners.get(t).add(s),()=>{this.listeners.get(t)?.delete(s)}}dispatch(t,s){this.listeners.get(t)?.forEach(e=>e(s))}}const u=new v;function p(r,t){r===401?u.dispatch("unauthenticated",t):r===403?u.dispatch("permissionDenied",t):r>=500&&u.dispatch("serverError",t)}function g(r,t,s){if(!s.length)return;const e=r.match(/^\s*(?:query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/)?.[1];for(const n of s){const a=n.path?.join(".")??"unknown",o=`[GraphQL] ${e??"operation"} — field error at ${a}: ${n.message}`;if(process.env.NODE_ENV==="development"){console.error(o,{error:n,request:{operationName:e,variables:t,query:r}});continue}console.error(o,{operationName:e,path:n.path,code:n.extensions?.code,debugMessage:n.extensions?.debugMessage})}}let d;function y(r){d=r}async function w(){if(!(typeof window<"u"||!d))try{const r=await d();return r?new Headers(r):void 0}catch{return}}async function q(r){return(await w())?.forEach((s,e)=>{r.has(e)||r.set(e,s)}),r}async function f(r){const{url:t,query:s,variables:e,requestHeaders:n,signal:a}=r,o=await q(new Headers(n));o.set("Content-Type","application/json"),o.has("Accept")||o.set("Accept","application/json");const c=await fetch(t,{method:"POST",headers:o,body:JSON.stringify({query:s,variables:e}),signal:a});let i;try{i=await c.json()}catch{i=void 0}if(!c.ok){const h=new l({status:c.status,data:i?.data,errors:i?.errors},{query:s,variables:e});throw p(c.status,{status:c.status,error:h}),h}if(i?.data)return i.errors?.length&&g(s,e,i.errors),i.data;throw new l({status:c.status,errors:i?.errors},{query:s,variables:e})}function E(){const r=async(e,n,a,o)=>f({url:e,query:String(n),variables:a,requestHeaders:o}),t=async e=>f({url:e.url,query:String(e.document),variables:e.variables,requestHeaders:e.requestHeaders,signal:e.signal}),s=r;return s.withOptions=t,s}function A(){return async function(t,s){const e=await w();let n=s;if(e){const o=new Headers(s?.headers);e.forEach((c,i)=>{o.has(i)||o.set(i,c)}),n={...s,headers:o}}const a=await fetch(t,n);return(a.status===401||a.status===403||a.status>=500)&&p(a.status,{status:a.status,response:a.clone()}),a}}export{l as ClientError,u as authAwareEvents,A as createAuthAwareFetch,E as createAuthAwareGraphqlClient,y as setServerRequestHeadersProvider};