tempest-react-sdk 0.76.0 → 0.78.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 +2 -2
- package/bin/create-tempest-app.e2e.test.mjs +112 -0
- package/bin/create-tempest-app.mjs +69 -28
- package/bin/lib/alias/collect.mjs +20 -3
- package/bin/lib/alias/index.mjs +12 -1
- package/bin/lib/alias/index.test.mjs +24 -0
- package/bin/lib/component/generate.mjs +191 -0
- package/bin/lib/component/generate.test.mjs +336 -0
- package/bin/lib/css/analyze.test.mjs +21 -1
- package/bin/lib/css/collect.mjs +14 -0
- package/bin/lib/css/extract.mjs +63 -5
- package/bin/lib/css/extract.test.mjs +110 -1
- package/bin/lib/css/fix.e2e.test.mjs +50 -0
- package/bin/lib/css/index.mjs +6 -1
- package/bin/lib/fs/contain.mjs +50 -0
- package/bin/lib/icons/generate.mjs +28 -3
- package/bin/lib/icons/generate.test.mjs +57 -1
- package/bin/lib/openapi/escape.mjs +224 -0
- package/bin/lib/openapi/generate.encode.test.mjs +120 -0
- package/bin/lib/openapi/generate.hostile.test.mjs +621 -0
- package/bin/lib/openapi/generate.mjs +129 -18
- package/bin/lib/openapi/generate.test.mjs +7 -2
- package/bin/lib/openapi/schema-to-zod.mjs +43 -4
- package/bin/tempest-gen.test.mjs +188 -0
- package/bin/tempest.mjs +263 -6
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +203 -58
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/refresh-queue.cjs +1 -1
- package/dist/auth/refresh-queue.cjs.map +1 -1
- package/dist/auth/refresh-queue.js +13 -6
- package/dist/auth/refresh-queue.js.map +1 -1
- package/dist/br/pix.cjs +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js +85 -72
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +19 -4
- package/dist/components/Chat/ChatAttachments.cjs +1 -1
- package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
- package/dist/components/Chat/ChatAttachments.js +46 -38
- package/dist/components/Chat/ChatAttachments.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/CodeBlock/tokenize.cjs +1 -1
- package/dist/components/CodeBlock/tokenize.cjs.map +1 -1
- package/dist/components/CodeBlock/tokenize.js +54 -25
- package/dist/components/CodeBlock/tokenize.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs +7 -5
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js +171 -90
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Toast/Toast.module.cjs +1 -1
- package/dist/components/Toast/Toast.module.cjs.map +1 -1
- package/dist/components/Toast/Toast.module.js +7 -3
- package/dist/components/Toast/Toast.module.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js +77 -51
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/data/create-data-provider.cjs +1 -1
- package/dist/data/create-data-provider.cjs.map +1 -1
- package/dist/data/create-data-provider.js +32 -21
- package/dist/data/create-data-provider.js.map +1 -1
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +81 -73
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js +39 -35
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js +141 -105
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +5 -6
- package/dist/http/retry.js.map +1 -1
- package/dist/oauth/callback.cjs +2 -0
- package/dist/oauth/callback.cjs.map +1 -0
- package/dist/oauth/callback.js +42 -0
- package/dist/oauth/callback.js.map +1 -0
- package/dist/oauth/use-oauth-callback.cjs +1 -1
- package/dist/oauth/use-oauth-callback.cjs.map +1 -1
- package/dist/oauth/use-oauth-callback.js +13 -20
- package/dist/oauth/use-oauth-callback.js.map +1 -1
- package/dist/push/use-push-subscription.cjs +1 -1
- package/dist/push/use-push-subscription.cjs.map +1 -1
- package/dist/push/use-push-subscription.js +42 -42
- package/dist/push/use-push-subscription.js.map +1 -1
- package/dist/push/web-push-client.cjs +1 -1
- package/dist/push/web-push-client.cjs.map +1 -1
- package/dist/push/web-push-client.js +23 -3
- package/dist/push/web-push-client.js.map +1 -1
- package/dist/query/offline-persistence.cjs +1 -1
- package/dist/query/offline-persistence.cjs.map +1 -1
- package/dist/query/offline-persistence.js +68 -23
- package/dist/query/offline-persistence.js.map +1 -1
- package/dist/styles/component/Toast.css +6 -1
- package/dist/styles/feedback.css +6 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js +272 -60
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache-inspect.cjs.map +1 -1
- package/dist/sw/cache-inspect.js.map +1 -1
- package/dist/sw/cache.cjs +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js +91 -64
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw/credential-headers.cjs +2 -0
- package/dist/sw/credential-headers.cjs.map +1 -0
- package/dist/sw/credential-headers.js +17 -0
- package/dist/sw/credential-headers.js.map +1 -0
- package/dist/sw/session-data.cjs +2 -0
- package/dist/sw/session-data.cjs.map +1 -0
- package/dist/sw/session-data.js +17 -0
- package/dist/sw/session-data.js.map +1 -0
- package/dist/sw.cjs +1 -1
- package/dist/sw.d.ts +309 -23
- package/dist/sw.js +7 -5
- package/dist/telemetry/app-error-queue.cjs +1 -1
- package/dist/telemetry/app-error-queue.cjs.map +1 -1
- package/dist/telemetry/app-error-queue.js +19 -14
- package/dist/telemetry/app-error-queue.js.map +1 -1
- package/dist/telemetry/app-error-reporter.cjs +1 -1
- package/dist/telemetry/app-error-reporter.cjs.map +1 -1
- package/dist/telemetry/app-error-reporter.js +75 -34
- package/dist/telemetry/app-error-reporter.js.map +1 -1
- package/dist/telemetry/app-error-transport.cjs +1 -1
- package/dist/telemetry/app-error-transport.cjs.map +1 -1
- package/dist/telemetry/app-error-transport.js +16 -3
- package/dist/telemetry/app-error-transport.js.map +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +1183 -64
- package/dist/tempest-react-sdk.js +160 -156
- package/dist/utils/csv.cjs +3 -3
- package/dist/utils/csv.cjs.map +1 -1
- package/dist/utils/csv.js +18 -14
- package/dist/utils/csv.js.map +1 -1
- package/dist/utils/owner-fingerprint.cjs +2 -0
- package/dist/utils/owner-fingerprint.cjs.map +1 -0
- package/dist/utils/owner-fingerprint.js +18 -0
- package/dist/utils/owner-fingerprint.js.map +1 -0
- package/dist/utils/safe-url.cjs +2 -0
- package/dist/utils/safe-url.cjs.map +1 -0
- package/dist/utils/safe-url.js +39 -0
- package/dist/utils/safe-url.js.map +1 -0
- package/dist/vite/tempest-csp.cjs +2 -2
- package/dist/vite/tempest-csp.cjs.map +1 -1
- package/dist/vite/tempest-csp.js +130 -64
- package/dist/vite/tempest-csp.js.map +1 -1
- package/dist/vite.d.ts +23 -5
- package/dist/webrtc/peer-link.cjs +1 -1
- package/dist/webrtc/peer-link.cjs.map +1 -1
- package/dist/webrtc/peer-link.js +2 -10
- package/dist/webrtc/peer-link.js.map +1 -1
- package/dist/webrtc/peer-mesh.cjs +1 -1
- package/dist/webrtc/peer-mesh.cjs.map +1 -1
- package/dist/webrtc/peer-mesh.js +110 -72
- package/dist/webrtc/peer-mesh.js.map +1 -1
- package/package.json +1 -1
- package/template-pwa/src/pwa.tsx +22 -0
- package/template-pwa/src/sw.ts +27 -14
- package/dist/components/Markdown/markdown-url.cjs +0 -2
- package/dist/components/Markdown/markdown-url.cjs.map +0 -1
- package/dist/components/Markdown/markdown-url.js +0 -28
- package/dist/components/Markdown/markdown-url.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("../Portal/portal-host.cjs"),n=require("./Toast.module.cjs");let r=require("react"),i=require("react/jsx-runtime"),a=require("react-dom");var o=(0,r.createContext)(null);function s(){let e=(0,r.useContext)(o);if(!e)throw Error(`useToast must be used inside a <ToastProvider>`);return e}function c({children:e,defaultDuration:t=4e3,position:n=`top-right`}){let[a,s]=(0,r.useState)([]),c=(0,r.useRef)(0),l=(0,r.useCallback)(e=>{s(t=>t.filter(t=>t.id!==e))},[]),d=(0,r.useCallback)(e=>{let n=e.id??`toast-${++c.current}`,r={id:n,title:e.title,description:e.description,variant:e.variant??`info`,duration:e.duration??t};return s(e=>[...e,r]),n},[t]),f=(0,r.useMemo)(()=>({show:d,dismiss:l,success:(e,t)=>d({...t,description:e,variant:`success`}),error:(e,t)=>d({...t,description:e,variant:`error`}),warning:(e,t)=>d({...t,description:e,variant:`warning`}),info:(e,t)=>d({...t,description:e,variant:`info`})}),[d,l]);return(0,i.jsxs)(o.Provider,{value:f,children:[e,(0,i.jsx)(u,{toasts:a,onDismiss:l,position:n})]})}function l(e){switch(e){case`top-left`:return n.default.positionTopLeft;case`top-center`:return n.default.positionTopCenter;case`bottom-right`:return n.default.positionBottomRight;case`bottom-left`:return n.default.positionBottomLeft;case`bottom-center`:return n.default.positionBottomCenter;default:return n.default.positionTopRight}}function u({toasts:r,onDismiss:o,position:s}){let c=t.usePortalHost();return c?(0,a.createPortal)((0,i.jsx)(`div`,{className:e.cn(n.default.container,l(s)),"aria-live":`polite`,"aria-atomic":`false`,children:r.map(e=>(0,i.jsx)(d,{toast:e,onDismiss:o},e.id))}),c):null}function d({toast:t,onDismiss:a}){
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("../Portal/portal-host.cjs"),n=require("./Toast.module.cjs");let r=require("react"),i=require("react/jsx-runtime"),a=require("react-dom");var o=(0,r.createContext)(null);function s(){let e=(0,r.useContext)(o);if(!e)throw Error(`useToast must be used inside a <ToastProvider>`);return e}function c({children:e,defaultDuration:t=4e3,position:n=`top-right`}){let[a,s]=(0,r.useState)([]),c=(0,r.useRef)(0),l=(0,r.useCallback)(e=>{s(t=>t.filter(t=>t.id!==e))},[]),d=(0,r.useCallback)(e=>{let n=e.id??`toast-${++c.current}`,r={id:n,title:e.title,description:e.description,variant:e.variant??`info`,duration:e.duration??t,icon:e.icon,iconLabel:e.iconLabel,align:e.align??`start`};return s(e=>[...e,r]),n},[t]),f=(0,r.useMemo)(()=>({show:d,dismiss:l,success:(e,t)=>d({...t,description:e,variant:`success`}),error:(e,t)=>d({...t,description:e,variant:`error`}),warning:(e,t)=>d({...t,description:e,variant:`warning`}),info:(e,t)=>d({...t,description:e,variant:`info`})}),[d,l]);return(0,i.jsxs)(o.Provider,{value:f,children:[e,(0,i.jsx)(u,{toasts:a,onDismiss:l,position:n})]})}function l(e){switch(e){case`top-left`:return n.default.positionTopLeft;case`top-center`:return n.default.positionTopCenter;case`bottom-right`:return n.default.positionBottomRight;case`bottom-left`:return n.default.positionBottomLeft;case`bottom-center`:return n.default.positionBottomCenter;default:return n.default.positionTopRight}}function u({toasts:r,onDismiss:o,position:s}){let c=t.usePortalHost();return c?(0,a.createPortal)((0,i.jsx)(`div`,{className:e.cn(n.default.container,l(s)),"aria-live":`polite`,"aria-atomic":`false`,children:r.map(e=>(0,i.jsx)(d,{toast:e,onDismiss:o},e.id))}),c):null}function d({toast:t,onDismiss:a}){(0,r.useEffect)(()=>{if(!t.duration)return;let e=setTimeout(()=>a(t.id),t.duration);return()=>clearTimeout(e)},[t.id,t.duration,a]);let o=f(t.icon);return(0,i.jsxs)(`div`,{className:e.cn(n.default.toast,n.default[t.variant],o&&n.default.withIcon,t.align===`center`&&n.default.alignCenter),children:[o&&(0,i.jsx)(`div`,{ref:p,className:n.default.leading,...t.iconLabel?{role:`img`,"aria-label":t.iconLabel}:{"aria-hidden":!0},children:t.icon}),(0,i.jsxs)(`div`,{className:n.default.content,children:[t.title&&(0,i.jsx)(`p`,{className:n.default.title,children:t.title}),t.description&&(0,i.jsx)(`p`,{className:n.default.description,children:t.description})]}),(0,i.jsx)(`button`,{type:`button`,className:n.default.close,"aria-label":`Fechar notificação`,onClick:()=>a(t.id),children:(0,i.jsx)(m,{})})]})}function f(e){return r.Children.toArray(e).some(e=>e===``?!1:(0,r.isValidElement)(e)&&e.type===r.Fragment?f(e.props.children):!0)}function p(e){e?.setAttribute(`inert`,``)}function m(){return(0,i.jsx)(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,children:(0,i.jsx)(`path`,{d:`M6 6l12 12M6 18L18 6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`})})}exports.ToastProvider=c,exports.useToast=s;
|
|
2
2
|
//# sourceMappingURL=ToastProvider.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToastProvider.cjs","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the provider is the queue: enqueue, dedupe, auto-\n * dismiss timers, pause on hover and the portal that renders them. Splitting the\n * queue from the renderer would put a timer in one file and the element it dismisses\n * in another.\n */\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n}\n\ninterface ToastEntry extends Required<Omit<ToastOptions, \"id\" | \"title\" | \"description\">> {\n id: string;\n title?: string;\n description?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n const portalHost = usePortalHost();\n\n if (!portalHost) return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n portalHost,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n return (\n <div className={cn(styles.toast, styles[toast.variant])}>\n <div>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ToastProvider.cjs","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the provider is the queue: enqueue, dedupe, auto-\n * dismiss timers, pause on hover and the portal that renders them. Splitting the\n * queue from the renderer would put a timer in one file and the element it dismisses\n * in another.\n */\nimport {\n Children,\n Fragment,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\n/**\n * Horizontal alignment of a toast's title and description.\n *\n * `\"start\"` is the default because a toast is read as a sentence beside a close\n * button: left-aligned lines share one edge with the icon and the stack, so a\n * two-line description stays scannable. `\"center\"` exists for the short\n * celebratory toast (an achievement with a badge) where the text is one line.\n */\nexport type ToastAlign = \"start\" | \"center\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n /**\n * Leading slot rendered before the text, e.g. `<img src=\"/badge.svg\" alt=\"\" />`.\n *\n * It is an app asset, so it takes any `ReactNode` — but it is a slot for an\n * **icon**, not for a control. The wrapper is `inert`, so a button or link put\n * here is neither focusable nor clickable; actions belong outside the toast.\n *\n * The slot is decorative (`aria-hidden`) unless {@link ToastOptions.iconLabel}\n * is given: most icons repeat what the title already says, and an unlabelled\n * image inside the live region would otherwise be announced as \"image\" or by\n * its file name. An `<img>` or `<svg>` anywhere inside it is capped at 32px and\n * keeps its aspect ratio. A node that renders nothing (`null`, a boolean, `\"\"`,\n * `[]`, an empty fragment) counts as no icon, so the toast keeps two columns.\n */\n icon?: ReactNode;\n /**\n * Accessible name for {@link ToastOptions.icon} when the icon carries meaning\n * the text does not. With it the slot becomes `role=\"img\"` with this label and\n * is announced together with the toast; without it the slot is hidden from\n * assistive technology.\n */\n iconLabel?: string;\n /** Text alignment of title and description. Default `\"start\"`; see {@link ToastAlign}. */\n align?: ToastAlign;\n}\n\ninterface ToastEntry extends Required<\n Omit<ToastOptions, \"id\" | \"title\" | \"description\" | \"icon\" | \"iconLabel\">\n> {\n id: string;\n title?: string;\n description?: string;\n icon?: ReactNode;\n iconLabel?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n icon: options.icon,\n iconLabel: options.iconLabel,\n align: options.align ?? \"start\",\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n const portalHost = usePortalHost();\n\n if (!portalHost) return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n portalHost,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n *\n * With an icon the toast becomes three columns (leading, content, close) and the\n * icon and text are centred vertically against each other, so a 32px badge sits\n * level with a one-line title. Toasts without an icon keep the top-aligned layout\n * and the same size for short text; what changed for them is long text: the text\n * column now wraps a long word (`overflow-wrap: anywhere`) and the close button no\n * longer shrinks (it used to drop from 22px to 16–17px beside a two-line\n * description).\n *\n * The leading wrapper gets `inert` through a callback ref instead of a prop,\n * because no prop value works on both supported Reacts: React 18 drops\n * `inert={true}` with a warning and renders `inert=\"\"`, while React 19 renders\n * `inert={true}` and drops `inert=\"\"`.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n const hasIcon = rendersSomething(toast.icon);\n\n return (\n <div\n className={cn(\n styles.toast,\n styles[toast.variant],\n hasIcon && styles.withIcon,\n toast.align === \"center\" && styles.alignCenter,\n )}\n >\n {hasIcon && (\n <div\n ref={makeInert}\n className={styles.leading}\n {...(toast.iconLabel\n ? { role: \"img\", \"aria-label\": toast.iconLabel }\n : { \"aria-hidden\": true })}\n >\n {toast.icon}\n </div>\n )}\n <div className={styles.content}>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\n/**\n * Whether a node renders anything at all.\n *\n * `Children.toArray` drops `null`, `undefined` and booleans and flattens arrays,\n * but keeps a fragment as one element — so a fragment is unwrapped and checked\n * by its children. `\"\"` renders no text node worth a column either.\n *\n * @param node - The icon slot content.\n * @returns `true` when at least one child would produce output.\n */\nfunction rendersSomething(node: ReactNode): boolean {\n return Children.toArray(node).some((child) => {\n if (child === \"\") return false;\n if (isValidElement<{ children?: ReactNode }>(child) && child.type === Fragment) {\n return rendersSomething(child.props.children);\n }\n return true;\n });\n}\n\n/**\n * Callback ref that marks the icon wrapper `inert`, on React 18 and 19 alike.\n *\n * @param node - The wrapper element, or `null` on unmount.\n */\nfunction makeInert(node: HTMLDivElement | null): void {\n node?.setAttribute(\"inert\", \"\");\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,62 +1,65 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
2
|
import { usePortalHost as t } from "../Portal/portal-host.js";
|
|
3
3
|
import n from "./Toast.module.js";
|
|
4
|
-
import {
|
|
5
|
-
import { jsx as
|
|
6
|
-
import { createPortal as
|
|
4
|
+
import { Children as r, Fragment as i, createContext as a, isValidElement as o, useCallback as s, useContext as c, useEffect as l, useMemo as u, useRef as d, useState as f } from "react";
|
|
5
|
+
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
6
|
+
import { createPortal as h } from "react-dom";
|
|
7
7
|
//#region src/components/Toast/ToastProvider.tsx
|
|
8
|
-
var
|
|
9
|
-
function
|
|
10
|
-
let e =
|
|
8
|
+
var g = a(null);
|
|
9
|
+
function _() {
|
|
10
|
+
let e = c(g);
|
|
11
11
|
if (!e) throw Error("useToast must be used inside a <ToastProvider>");
|
|
12
12
|
return e;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
let [r,
|
|
16
|
-
|
|
17
|
-
}, []),
|
|
18
|
-
let n = e.id ?? `toast-${++
|
|
14
|
+
function v({ children: e, defaultDuration: t = 4e3, position: n = "top-right" }) {
|
|
15
|
+
let [r, i] = f([]), a = d(0), o = s((e) => {
|
|
16
|
+
i((t) => t.filter((t) => t.id !== e));
|
|
17
|
+
}, []), c = s((e) => {
|
|
18
|
+
let n = e.id ?? `toast-${++a.current}`, r = {
|
|
19
19
|
id: n,
|
|
20
20
|
title: e.title,
|
|
21
21
|
description: e.description,
|
|
22
22
|
variant: e.variant ?? "info",
|
|
23
|
-
duration: e.duration ?? t
|
|
23
|
+
duration: e.duration ?? t,
|
|
24
|
+
icon: e.icon,
|
|
25
|
+
iconLabel: e.iconLabel,
|
|
26
|
+
align: e.align ?? "start"
|
|
24
27
|
};
|
|
25
|
-
return
|
|
26
|
-
}, [t]),
|
|
27
|
-
show:
|
|
28
|
-
dismiss:
|
|
29
|
-
success: (e, t) =>
|
|
28
|
+
return i((e) => [...e, r]), n;
|
|
29
|
+
}, [t]), l = u(() => ({
|
|
30
|
+
show: c,
|
|
31
|
+
dismiss: o,
|
|
32
|
+
success: (e, t) => c({
|
|
30
33
|
...t,
|
|
31
34
|
description: e,
|
|
32
35
|
variant: "success"
|
|
33
36
|
}),
|
|
34
|
-
error: (e, t) =>
|
|
37
|
+
error: (e, t) => c({
|
|
35
38
|
...t,
|
|
36
39
|
description: e,
|
|
37
40
|
variant: "error"
|
|
38
41
|
}),
|
|
39
|
-
warning: (e, t) =>
|
|
42
|
+
warning: (e, t) => c({
|
|
40
43
|
...t,
|
|
41
44
|
description: e,
|
|
42
45
|
variant: "warning"
|
|
43
46
|
}),
|
|
44
|
-
info: (e, t) =>
|
|
47
|
+
info: (e, t) => c({
|
|
45
48
|
...t,
|
|
46
49
|
description: e,
|
|
47
50
|
variant: "info"
|
|
48
51
|
})
|
|
49
|
-
}), [
|
|
50
|
-
return /* @__PURE__ */
|
|
51
|
-
value:
|
|
52
|
-
children: [e, /* @__PURE__ */
|
|
52
|
+
}), [c, o]);
|
|
53
|
+
return /* @__PURE__ */ m(g.Provider, {
|
|
54
|
+
value: l,
|
|
55
|
+
children: [e, /* @__PURE__ */ p(b, {
|
|
53
56
|
toasts: r,
|
|
54
|
-
onDismiss:
|
|
57
|
+
onDismiss: o,
|
|
55
58
|
position: n
|
|
56
59
|
})]
|
|
57
60
|
});
|
|
58
61
|
}
|
|
59
|
-
function
|
|
62
|
+
function y(e) {
|
|
60
63
|
switch (e) {
|
|
61
64
|
case "top-left": return n.positionTopLeft;
|
|
62
65
|
case "top-center": return n.positionTopCenter;
|
|
@@ -66,20 +69,20 @@ function g(e) {
|
|
|
66
69
|
default: return n.positionTopRight;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
|
-
function
|
|
72
|
+
function b({ toasts: r, onDismiss: i, position: a }) {
|
|
70
73
|
let o = t();
|
|
71
|
-
return o ?
|
|
72
|
-
className: e(n.container,
|
|
74
|
+
return o ? h(/* @__PURE__ */ p("div", {
|
|
75
|
+
className: e(n.container, y(a)),
|
|
73
76
|
"aria-live": "polite",
|
|
74
77
|
"aria-atomic": "false",
|
|
75
|
-
children: r.map((e) => /* @__PURE__ */
|
|
78
|
+
children: r.map((e) => /* @__PURE__ */ p(x, {
|
|
76
79
|
toast: e,
|
|
77
80
|
onDismiss: i
|
|
78
81
|
}, e.id))
|
|
79
82
|
}), o) : null;
|
|
80
83
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
84
|
+
function x({ toast: t, onDismiss: r }) {
|
|
85
|
+
l(() => {
|
|
83
86
|
if (!t.duration) return;
|
|
84
87
|
let e = setTimeout(() => r(t.id), t.duration);
|
|
85
88
|
return () => clearTimeout(e);
|
|
@@ -87,30 +90,53 @@ function v({ toast: t, onDismiss: r }) {
|
|
|
87
90
|
t.id,
|
|
88
91
|
t.duration,
|
|
89
92
|
r
|
|
90
|
-
])
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
]);
|
|
94
|
+
let i = S(t.icon);
|
|
95
|
+
return /* @__PURE__ */ m("div", {
|
|
96
|
+
className: e(n.toast, n[t.variant], i && n.withIcon, t.align === "center" && n.alignCenter),
|
|
97
|
+
children: [
|
|
98
|
+
i && /* @__PURE__ */ p("div", {
|
|
99
|
+
ref: C,
|
|
100
|
+
className: n.leading,
|
|
101
|
+
...t.iconLabel ? {
|
|
102
|
+
role: "img",
|
|
103
|
+
"aria-label": t.iconLabel
|
|
104
|
+
} : { "aria-hidden": !0 },
|
|
105
|
+
children: t.icon
|
|
106
|
+
}),
|
|
107
|
+
/* @__PURE__ */ m("div", {
|
|
108
|
+
className: n.content,
|
|
109
|
+
children: [t.title && /* @__PURE__ */ p("p", {
|
|
110
|
+
className: n.title,
|
|
111
|
+
children: t.title
|
|
112
|
+
}), t.description && /* @__PURE__ */ p("p", {
|
|
113
|
+
className: n.description,
|
|
114
|
+
children: t.description
|
|
115
|
+
})]
|
|
116
|
+
}),
|
|
117
|
+
/* @__PURE__ */ p("button", {
|
|
118
|
+
type: "button",
|
|
119
|
+
className: n.close,
|
|
120
|
+
"aria-label": "Fechar notificação",
|
|
121
|
+
onClick: () => r(t.id),
|
|
122
|
+
children: /* @__PURE__ */ p(w, {})
|
|
123
|
+
})
|
|
124
|
+
]
|
|
105
125
|
});
|
|
106
126
|
}
|
|
107
|
-
function
|
|
108
|
-
return
|
|
127
|
+
function S(e) {
|
|
128
|
+
return r.toArray(e).some((e) => e === "" ? !1 : o(e) && e.type === i ? S(e.props.children) : !0);
|
|
129
|
+
}
|
|
130
|
+
function C(e) {
|
|
131
|
+
e?.setAttribute("inert", "");
|
|
132
|
+
}
|
|
133
|
+
function w() {
|
|
134
|
+
return /* @__PURE__ */ p("svg", {
|
|
109
135
|
width: "14",
|
|
110
136
|
height: "14",
|
|
111
137
|
viewBox: "0 0 24 24",
|
|
112
138
|
fill: "none",
|
|
113
|
-
children: /* @__PURE__ */
|
|
139
|
+
children: /* @__PURE__ */ p("path", {
|
|
114
140
|
d: "M6 6l12 12M6 18L18 6",
|
|
115
141
|
stroke: "currentColor",
|
|
116
142
|
strokeWidth: "2",
|
|
@@ -119,6 +145,6 @@ function y() {
|
|
|
119
145
|
});
|
|
120
146
|
}
|
|
121
147
|
//#endregion
|
|
122
|
-
export {
|
|
148
|
+
export { v as ToastProvider, _ as useToast };
|
|
123
149
|
|
|
124
150
|
//# sourceMappingURL=ToastProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ToastProvider.js","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the provider is the queue: enqueue, dedupe, auto-\n * dismiss timers, pause on hover and the portal that renders them. Splitting the\n * queue from the renderer would put a timer in one file and the element it dismisses\n * in another.\n */\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n}\n\ninterface ToastEntry extends Required<Omit<ToastOptions, \"id\" | \"title\" | \"description\">> {\n id: string;\n title?: string;\n description?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n const portalHost = usePortalHost();\n\n if (!portalHost) return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n portalHost,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n return (\n <div className={cn(styles.toast, styles[toast.variant])}>\n <div>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ToastProvider.js","names":[],"sources":["../../../src/components/Toast/ToastProvider.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines — the provider is the queue: enqueue, dedupe, auto-\n * dismiss timers, pause on hover and the portal that renders them. Splitting the\n * queue from the renderer would put a timer in one file and the element it dismisses\n * in another.\n */\nimport {\n Children,\n Fragment,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type { ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Toast.module.css\";\nimport { usePortalHost } from \"../Portal/portal-host\";\n\nexport type ToastVariant = \"success\" | \"warning\" | \"error\" | \"info\";\n\n/**\n * Horizontal alignment of a toast's title and description.\n *\n * `\"start\"` is the default because a toast is read as a sentence beside a close\n * button: left-aligned lines share one edge with the icon and the stack, so a\n * two-line description stays scannable. `\"center\"` exists for the short\n * celebratory toast (an achievement with a badge) where the text is one line.\n */\nexport type ToastAlign = \"start\" | \"center\";\n\nexport interface ToastOptions {\n id?: string;\n title?: string;\n description?: string;\n variant?: ToastVariant;\n /** Auto-dismiss timeout in ms. Pass `0` to keep the toast until dismissed manually. */\n duration?: number;\n /**\n * Leading slot rendered before the text, e.g. `<img src=\"/badge.svg\" alt=\"\" />`.\n *\n * It is an app asset, so it takes any `ReactNode` — but it is a slot for an\n * **icon**, not for a control. The wrapper is `inert`, so a button or link put\n * here is neither focusable nor clickable; actions belong outside the toast.\n *\n * The slot is decorative (`aria-hidden`) unless {@link ToastOptions.iconLabel}\n * is given: most icons repeat what the title already says, and an unlabelled\n * image inside the live region would otherwise be announced as \"image\" or by\n * its file name. An `<img>` or `<svg>` anywhere inside it is capped at 32px and\n * keeps its aspect ratio. A node that renders nothing (`null`, a boolean, `\"\"`,\n * `[]`, an empty fragment) counts as no icon, so the toast keeps two columns.\n */\n icon?: ReactNode;\n /**\n * Accessible name for {@link ToastOptions.icon} when the icon carries meaning\n * the text does not. With it the slot becomes `role=\"img\"` with this label and\n * is announced together with the toast; without it the slot is hidden from\n * assistive technology.\n */\n iconLabel?: string;\n /** Text alignment of title and description. Default `\"start\"`; see {@link ToastAlign}. */\n align?: ToastAlign;\n}\n\ninterface ToastEntry extends Required<\n Omit<ToastOptions, \"id\" | \"title\" | \"description\" | \"icon\" | \"iconLabel\">\n> {\n id: string;\n title?: string;\n description?: string;\n icon?: ReactNode;\n iconLabel?: string;\n}\n\nexport interface ToastApi {\n show: (options: ToastOptions) => string;\n dismiss: (id: string) => void;\n success: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n error: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n warning: (\n description: string,\n options?: Omit<ToastOptions, \"variant\" | \"description\">,\n ) => string;\n info: (description: string, options?: Omit<ToastOptions, \"variant\" | \"description\">) => string;\n}\n\nconst ToastContext = createContext<ToastApi | null>(null);\n\n/**\n * Access the toast API. Must be used inside a {@link ToastProvider}.\n *\n * @returns Methods to show and dismiss toasts.\n */\nexport function useToast(): ToastApi {\n const ctx = useContext(ToastContext);\n if (!ctx) throw new Error(\"useToast must be used inside a <ToastProvider>\");\n return ctx;\n}\n\nexport type ToastPosition =\n \"top-right\" | \"top-left\" | \"top-center\" | \"bottom-right\" | \"bottom-left\" | \"bottom-center\";\n\nexport interface ToastProviderProps {\n children: ReactNode;\n /** Default auto-dismiss duration (ms). Default 4000. */\n defaultDuration?: number;\n /** Stack position on screen. Default `\"top-right\"`. */\n position?: ToastPosition;\n}\n\n/**\n * Renders a portalled toast container and exposes the imperative {@link useToast} API.\n */\nexport function ToastProvider({\n children,\n defaultDuration = 4000,\n position = \"top-right\",\n}: ToastProviderProps) {\n const [toasts, setToasts] = useState<ToastEntry[]>([]);\n const counter = useRef<number>(0);\n\n const dismiss = useCallback((id: string): void => {\n setToasts((current) => current.filter((toast) => toast.id !== id));\n }, []);\n\n const show = useCallback(\n (options: ToastOptions): string => {\n const id = options.id ?? `toast-${++counter.current}`;\n const entry: ToastEntry = {\n id,\n title: options.title,\n description: options.description,\n variant: options.variant ?? \"info\",\n duration: options.duration ?? defaultDuration,\n icon: options.icon,\n iconLabel: options.iconLabel,\n align: options.align ?? \"start\",\n };\n setToasts((current) => [...current, entry]);\n return id;\n },\n [defaultDuration],\n );\n\n const api = useMemo<ToastApi>(\n () => ({\n show,\n dismiss,\n success: (description, options) =>\n show({ ...options, description, variant: \"success\" }),\n error: (description, options) => show({ ...options, description, variant: \"error\" }),\n warning: (description, options) =>\n show({ ...options, description, variant: \"warning\" }),\n info: (description, options) => show({ ...options, description, variant: \"info\" }),\n }),\n [show, dismiss],\n );\n\n return (\n <ToastContext.Provider value={api}>\n {children}\n <ToastContainer toasts={toasts} onDismiss={dismiss} position={position} />\n </ToastContext.Provider>\n );\n}\n\ninterface ContainerProps {\n toasts: ToastEntry[];\n onDismiss: (id: string) => void;\n position: ToastPosition;\n}\n\nfunction positionClass(position: ToastPosition): string {\n switch (position) {\n case \"top-left\":\n return styles.positionTopLeft;\n case \"top-center\":\n return styles.positionTopCenter;\n case \"bottom-right\":\n return styles.positionBottomRight;\n case \"bottom-left\":\n return styles.positionBottomLeft;\n case \"bottom-center\":\n return styles.positionBottomCenter;\n case \"top-right\":\n default:\n return styles.positionTopRight;\n }\n}\n\n/**\n * The portalled stack, and the page's live region for toasts.\n *\n * `aria-atomic=\"false\"` matters: with `\"true\"` a screen reader re-reads the\n * **entire** stack on every change, so the third toast of a batch is announced as\n * all three, and dismissing one re-announces the survivors. `\"false\"` announces only\n * the node that was added, which is what the user needs.\n *\n * The stack stays the live region rather than delegating to `useAnnounce`, because\n * routing it through the shared announcer would put the same text in the document\n * twice — once visible, once hidden — and every `getByText(\"Salvo\")` in a consuming\n * app's test suite would start matching two nodes. The announcer is for messages\n * that have no on-screen home; a toast has one.\n */\nfunction ToastContainer({ toasts, onDismiss, position }: ContainerProps) {\n const portalHost = usePortalHost();\n\n if (!portalHost) return null;\n return createPortal(\n <div\n className={cn(styles.container, positionClass(position))}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n >\n {toasts.map((toast) => (\n <ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />\n ))}\n </div>,\n portalHost,\n );\n}\n\ninterface ItemProps {\n toast: ToastEntry;\n onDismiss: (id: string) => void;\n}\n\n/**\n * One toast.\n *\n * No `role=\"status\"` of its own: it already lives inside the container's live\n * region, and a live region nested in a live region is announced twice by some\n * screen readers.\n *\n * With an icon the toast becomes three columns (leading, content, close) and the\n * icon and text are centred vertically against each other, so a 32px badge sits\n * level with a one-line title. Toasts without an icon keep the top-aligned layout\n * and the same size for short text; what changed for them is long text: the text\n * column now wraps a long word (`overflow-wrap: anywhere`) and the close button no\n * longer shrinks (it used to drop from 22px to 16–17px beside a two-line\n * description).\n *\n * The leading wrapper gets `inert` through a callback ref instead of a prop,\n * because no prop value works on both supported Reacts: React 18 drops\n * `inert={true}` with a warning and renders `inert=\"\"`, while React 19 renders\n * `inert={true}` and drops `inert=\"\"`.\n */\nfunction ToastItem({ toast, onDismiss }: ItemProps) {\n useEffect(() => {\n if (!toast.duration) return;\n const timer = setTimeout(() => onDismiss(toast.id), toast.duration);\n return () => clearTimeout(timer);\n }, [toast.id, toast.duration, onDismiss]);\n\n const hasIcon = rendersSomething(toast.icon);\n\n return (\n <div\n className={cn(\n styles.toast,\n styles[toast.variant],\n hasIcon && styles.withIcon,\n toast.align === \"center\" && styles.alignCenter,\n )}\n >\n {hasIcon && (\n <div\n ref={makeInert}\n className={styles.leading}\n {...(toast.iconLabel\n ? { role: \"img\", \"aria-label\": toast.iconLabel }\n : { \"aria-hidden\": true })}\n >\n {toast.icon}\n </div>\n )}\n <div className={styles.content}>\n {toast.title && <p className={styles.title}>{toast.title}</p>}\n {toast.description && <p className={styles.description}>{toast.description}</p>}\n </div>\n <button\n type=\"button\"\n className={styles.close}\n aria-label=\"Fechar notificação\"\n onClick={() => onDismiss(toast.id)}\n >\n <CloseIcon />\n </button>\n </div>\n );\n}\n\n/**\n * Whether a node renders anything at all.\n *\n * `Children.toArray` drops `null`, `undefined` and booleans and flattens arrays,\n * but keeps a fragment as one element — so a fragment is unwrapped and checked\n * by its children. `\"\"` renders no text node worth a column either.\n *\n * @param node - The icon slot content.\n * @returns `true` when at least one child would produce output.\n */\nfunction rendersSomething(node: ReactNode): boolean {\n return Children.toArray(node).some((child) => {\n if (child === \"\") return false;\n if (isValidElement<{ children?: ReactNode }>(child) && child.type === Fragment) {\n return rendersSomething(child.props.children);\n }\n return true;\n });\n}\n\n/**\n * Callback ref that marks the icon wrapper `inert`, on React 18 and 19 alike.\n *\n * @param node - The wrapper element, or `null` on unmount.\n */\nfunction makeInert(node: HTMLDivElement | null): void {\n node?.setAttribute(\"inert\", \"\");\n}\n\nfunction CloseIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 6l12 12M6 18L18 6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;;;AA8FA,IAAM,IAAe,EAA+B,IAAI;AAOxD,SAAgB,IAAqB;CACjC,IAAM,IAAM,EAAW,CAAY;CACnC,IAAI,CAAC,GAAK,MAAU,MAAM,gDAAgD;CAC1E,OAAO;AACX;AAgBA,SAAgB,EAAc,EAC1B,aACA,qBAAkB,KAClB,cAAW,eACQ;CACnB,IAAM,CAAC,GAAQ,KAAa,EAAuB,CAAC,CAAC,GAC/C,IAAU,EAAe,CAAC,GAE1B,IAAU,GAAa,MAAqB;EAC9C,GAAW,MAAY,EAAQ,QAAQ,MAAU,EAAM,OAAO,CAAE,CAAC;CACrE,GAAG,CAAC,CAAC,GAEC,IAAO,GACR,MAAkC;EAC/B,IAAM,IAAK,EAAQ,MAAM,SAAS,EAAE,EAAQ,WACtC,IAAoB;GACtB;GACA,OAAO,EAAQ;GACf,aAAa,EAAQ;GACrB,SAAS,EAAQ,WAAW;GAC5B,UAAU,EAAQ,YAAY;GAC9B,MAAM,EAAQ;GACd,WAAW,EAAQ;GACnB,OAAO,EAAQ,SAAS;EAC5B;EAEA,OADA,GAAW,MAAY,CAAC,GAAG,GAAS,CAAK,CAAC,GACnC;CACX,GACA,CAAC,CAAe,CACpB,GAEM,IAAM,SACD;EACH;EACA;EACA,UAAU,GAAa,MACnB,EAAK;GAAE,GAAG;GAAS;GAAa,SAAS;EAAU,CAAC;EACxD,QAAQ,GAAa,MAAY,EAAK;GAAE,GAAG;GAAS;GAAa,SAAS;EAAQ,CAAC;EACnF,UAAU,GAAa,MACnB,EAAK;GAAE,GAAG;GAAS;GAAa,SAAS;EAAU,CAAC;EACxD,OAAO,GAAa,MAAY,EAAK;GAAE,GAAG;GAAS;GAAa,SAAS;EAAO,CAAC;CACrF,IACA,CAAC,GAAM,CAAO,CAClB;CAEA,OACI,kBAAC,EAAa,UAAd;EAAuB,OAAO;EAA9B,UAAA,CACK,GACD,kBAAC,GAAD;GAAwB;GAAQ,WAAW;GAAmB;EAAW,CAAA,CACtD;;AAE/B;AAQA,SAAS,EAAc,GAAiC;CACpD,QAAQ,GAAR;EACI,KAAK,YACD,OAAO,EAAO;EAClB,KAAK,cACD,OAAO,EAAO;EAClB,KAAK,gBACD,OAAO,EAAO;EAClB,KAAK,eACD,OAAO,EAAO;EAClB,KAAK,iBACD,OAAO,EAAO;EAElB,SACI,OAAO,EAAO;CACtB;AACJ;AAgBA,SAAS,EAAe,EAAE,WAAQ,cAAW,eAA4B;CACrE,IAAM,IAAa,EAAc;CAGjC,OADK,IACE,EACH,kBAAC,OAAD;EACI,WAAW,EAAG,EAAO,WAAW,EAAc,CAAQ,CAAC;EACvD,aAAU;EACV,eAAY;EAEX,UAAA,EAAO,KAAK,MACT,kBAAC,GAAD;GAAiC;GAAkB;EAAY,GAA/C,EAAM,EAAyC,CAClE;CACA,CAAA,GACL,CACJ,IAZwB;AAa5B;AA2BA,SAAS,EAAU,EAAE,UAAO,gBAAwB;CAChD,QAAgB;EACZ,IAAI,CAAC,EAAM,UAAU;EACrB,IAAM,IAAQ,iBAAiB,EAAU,EAAM,EAAE,GAAG,EAAM,QAAQ;EAClE,aAAa,aAAa,CAAK;CACnC,GAAG;EAAC,EAAM;EAAI,EAAM;EAAU;CAAS,CAAC;CAExC,IAAM,IAAU,EAAiB,EAAM,IAAI;CAE3C,OACI,kBAAC,OAAD;EACI,WAAW,EACP,EAAO,OACP,EAAO,EAAM,UACb,KAAW,EAAO,UAClB,EAAM,UAAU,YAAY,EAAO,WACvC;EANJ,UAAA;GAQK,KACG,kBAAC,OAAD;IACI,KAAK;IACL,WAAW,EAAO;IAClB,GAAK,EAAM,YACL;KAAE,MAAM;KAAO,cAAc,EAAM;IAAU,IAC7C,EAAE,eAAe,GAAK;IAE3B,UAAA,EAAM;GACN,CAAA;GAET,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACK,EAAM,SAAS,kBAAC,KAAD;KAAG,WAAW,EAAO;KAAQ,UAAA,EAAM;IAAS,CAAA,GAC3D,EAAM,eAAe,kBAAC,KAAD;KAAG,WAAW,EAAO;KAAc,UAAA,EAAM;IAAe,CAAA,CAC7E;;GACL,kBAAC,UAAD;IACI,MAAK;IACL,WAAW,EAAO;IAClB,cAAW;IACX,eAAe,EAAU,EAAM,EAAE;IAEjC,UAAA,kBAAC,GAAD,CAAY,CAAA;GACR,CAAA;EACP;;AAEb;AAYA,SAAS,EAAiB,GAA0B;CAChD,OAAO,EAAS,QAAQ,CAAI,CAAC,CAAC,MAAM,MAC5B,MAAU,KAAW,KACrB,EAAyC,CAAK,KAAK,EAAM,SAAS,IAC3D,EAAiB,EAAM,MAAM,QAAQ,IAEzC,EACV;AACL;AAOA,SAAS,EAAU,GAAmC;CAClD,GAAM,aAAa,SAAS,EAAE;AAClC;AAEA,SAAS,IAAY;CACjB,OACI,kBAAC,OAAD;EAAK,OAAM;EAAK,QAAO;EAAK,SAAQ;EAAY,MAAK;EACjD,UAAA,kBAAC,QAAD;GACI,GAAE;GACF,QAAO;GACP,aAAY;GACZ,eAAc;EACjB,CAAA;CACA,CAAA;AAEb"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var e=(e,t)=>t==null?`/${e}`:`/${e}/${t}`;function
|
|
1
|
+
var e=new Set([``,`.`,`..`]);function t(t){if(t==null)throw TypeError(`Invalid record id ${String(t)}: a record id is required.`);let n=String(t);if(e.has(n))throw TypeError(`Invalid record id ${JSON.stringify(n)}: it would resolve as a path dot-segment.`);return encodeURIComponent(n)}var n=(e,t)=>t==null?`/${e}`:`/${e}/${t}`;function r(e,r={}){let{pageParam:i=`page`,sizeParam:a=`size`,sortFieldParam:o=`order_by`,sortOrderParam:s=`ascending`,sortOrderAsBoolean:c=!0,updateMethod:l=`patch`,buildPath:u=n}=r,d={getList(t,n={}){let{pagination:r,sort:l,filters:d}=n,f={};if(r?.page!=null&&(f[i]=r.page),r?.pageSize!=null&&(f[a]=r.pageSize),l?.field){let e=l.order??`asc`;f[o]=l.field,f[s]=c?e===`asc`:e}if(d)for(let[e,t]of Object.entries(d))f[e]=t;let p={params:f};return e.get(u(t),p)},async getOne(n,r){return e.get(u(n,t(r)))},getMany(e,t){return Promise.all(t.map(t=>d.getOne(e,t)))},create(t,n){return e.post(u(t),{body:n})},async update(n,r,i){let a=u(n,t(r));return l===`put`?e.put(a,{body:i}):e.patch(a,{body:i})},async deleteOne(n,r){return e.delete(u(n,t(r)))}};return d}exports.createDataProvider=r,exports.encodeIdSegment=t;
|
|
2
2
|
//# sourceMappingURL=create-data-provider.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-data-provider.cjs","names":[],"sources":["../../src/data/create-data-provider.ts"],"sourcesContent":["import type { ApiClient, RequestOptions } from \"@/http/types\";\nimport type { OffsetPage } from \"@/query/pagination\";\n\n/** Query-param values accepted by the API client for a list request. */\ntype ParamValue = string | number | boolean | undefined | null;\n\n/** Filters passed to {@link DataProvider.getList}, spread verbatim into the query string. */\nexport type DataFilters = Record<string, ParamValue>;\n\n/** Parameters for a paginated, sorted, filtered list request. */\nexport interface GetListParams {\n /** Offset pagination — 1-based `page` and `pageSize`. */\n pagination?: { page?: number; pageSize?: number };\n /** Single-field sort. `order` defaults to `\"asc\"`. */\n sort?: { field: string; order?: \"asc\" | \"desc\" };\n /** Arbitrary filter query params, spread onto the request. */\n filters?: DataFilters;\n}\n\n/**\n * Refine-style data provider over the Tempest FastAPI SDK CRUD/pagination\n * conventions. Implementations are stateless wrappers around an {@link ApiClient}.\n */\nexport interface DataProvider {\n /**\n * Fetch a paginated list of a resource.\n *\n * @param resource - The resource name (e.g. `\"users\"`).\n * @param params - Pagination, sort and filter parameters.\n * @returns The offset-paginated envelope.\n */\n getList<T>(resource: string, params?: GetListParams): Promise<OffsetPage<T>>;\n /**\n * Fetch a single record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The record.\n */\n getOne<T>(resource: string, id: string | number): Promise<T>;\n /**\n * Fetch many records by id (default: parallel {@link DataProvider.getOne}).\n *\n * @param resource - The resource name.\n * @param ids - The record ids.\n * @returns The records, in the same order as `ids`.\n */\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]>;\n /**\n * Create a record.\n *\n * @param resource - The resource name.\n * @param data - The creation payload.\n * @returns The created record.\n */\n create<T>(resource: string, data: unknown): Promise<T>;\n /**\n * Update a record (PATCH by default, PUT when configured).\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @param data - The update payload.\n * @returns The updated record.\n */\n update<T>(resource: string, id: string | number, data: unknown): Promise<T>;\n /**\n * Delete a record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The delete response (often the deleted record).\n */\n deleteOne<T>(resource: string, id: string | number): Promise<T>;\n}\n\n/** Options to tailor {@link createDataProvider} to a backend's conventions. */\nexport interface DataProviderOptions {\n /** Query-param name for the page number. Default: `\"page\"`. */\n pageParam?: string;\n /** Query-param name for the page size. Default: `\"size\"`. */\n sizeParam?: string;\n /** Query-param name for the sort field. Default: `\"order_by\"`. */\n sortFieldParam?: string;\n /** Query-param name for the sort order. Default: `\"ascending\"`. */\n sortOrderParam?: string;\n /**\n * When `true` (default), emit the sort order as a boolean\n * (`order:\"asc\"` → `true`). When `false`, emit the literal `\"asc\"`/`\"desc\"`.\n */\n sortOrderAsBoolean?: boolean;\n /** HTTP method used by {@link DataProvider.update}. Default: `\"patch\"`. */\n updateMethod?: \"patch\" | \"put\";\n /**\n * Build the request path for a resource (and optional id).\n * Default: `id == null ? \"/\" + resource : \"/\" + resource + \"/\" + id`.\n */\n buildPath?: (resource: string, id?: string | number) => string;\n}\n\nconst defaultBuildPath = (resource: string, id?: string | number): string =>\n id == null ? `/${resource}` : `/${resource}/${id}`;\n\n/**\n * Create a {@link DataProvider} bound to an {@link ApiClient}.\n *\n * Maps Refine-style calls to the Tempest FastAPI SDK conventions: list →\n * `GET /{resource}?page=&size=&order_by=&ascending=&...filters`, one →\n * `GET /{resource}/{id}`, create → `POST`, update → `PATCH`/`PUT`,\n * delete → `DELETE`.\n *\n * @param client - The HTTP client created by `createApiClient`.\n * @param options - Optional overrides for param names, sort encoding,\n * update method and path building.\n * @returns A stateless data provider.\n *\n * @example\n * const dataProvider = createDataProvider(apiClient);\n * const page = await dataProvider.getList<User>(\"users\", {\n * pagination: { page: 1, pageSize: 20 },\n * sort: { field: \"created_at\", order: \"desc\" },\n * filters: { active: true },\n * });\n */\nexport function createDataProvider(\n client: ApiClient,\n options: DataProviderOptions = {},\n): DataProvider {\n const {\n pageParam = \"page\",\n sizeParam = \"size\",\n sortFieldParam = \"order_by\",\n sortOrderParam = \"ascending\",\n sortOrderAsBoolean = true,\n updateMethod = \"patch\",\n buildPath = defaultBuildPath,\n } = options;\n\n const provider: DataProvider = {\n getList<T>(resource: string, params: GetListParams = {}): Promise<OffsetPage<T>> {\n const { pagination, sort, filters } = params;\n const query: Record<string, ParamValue> = {};\n\n if (pagination?.page != null) query[pageParam] = pagination.page;\n if (pagination?.pageSize != null) query[sizeParam] = pagination.pageSize;\n\n if (sort?.field) {\n const order = sort.order ?? \"asc\";\n query[sortFieldParam] = sort.field;\n query[sortOrderParam] = sortOrderAsBoolean ? order === \"asc\" : order;\n }\n\n if (filters) {\n for (const [key, value] of Object.entries(filters)) {\n query[key] = value;\n }\n }\n\n const requestOptions: RequestOptions = { params: query };\n return client.get<OffsetPage<T>>(buildPath(resource), requestOptions);\n },\n\n getOne<T>(resource: string, id: string | number): Promise<T> {\n return client.get<T>(buildPath(resource, id));\n },\n\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]> {\n return Promise.all(ids.map((id) => provider.getOne<T>(resource, id)));\n },\n\n create<T>(resource: string, data: unknown): Promise<T> {\n return client.post<T>(buildPath(resource), { body: data });\n },\n\n update<T>(resource: string, id: string | number, data: unknown): Promise<T> {\n const path = buildPath(resource, id);\n return updateMethod === \"put\"\n ? client.put<T>(path, { body: data })\n : client.patch<T>(path, { body: data });\n },\n\n deleteOne<T>(resource: string, id: string | number): Promise<T> {\n return client.delete<T>(buildPath(resource, id));\n },\n };\n\n return provider;\n}\n"],"mappings":"AAmGA,IAAM,GAAoB,EAAkB,IACxC,GAAM,KAAO,IAAI,IAAa,IAAI,EAAS,GAAG,IAuBlD,SAAgB,EACZ,EACA,EAA+B,CAAC,EACpB,CACZ,GAAM,CACF,YAAY,OACZ,YAAY,OACZ,iBAAiB,WACjB,iBAAiB,YACjB,qBAAqB,GACrB,eAAe,QACf,YAAY,GACZ,EAEE,EAAyB,CAC3B,QAAW,EAAkB,EAAwB,CAAC,EAA2B,CAC7E,GAAM,CAAE,aAAY,OAAM,WAAY,EAChC,EAAoC,CAAC,EAK3C,GAHI,GAAY,MAAQ,OAAM,EAAM,GAAa,EAAW,MACxD,GAAY,UAAY,OAAM,EAAM,GAAa,EAAW,UAE5D,GAAM,MAAO,CACb,IAAM,EAAQ,EAAK,OAAS,MAC5B,EAAM,GAAkB,EAAK,MAC7B,EAAM,GAAkB,EAAqB,IAAU,MAAQ,CACnE,CAEA,GAAI,EACA,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAO,EAC7C,EAAM,GAAO,EAIrB,IAAM,EAAiC,CAAE,OAAQ,CAAM,EACvD,OAAO,EAAO,IAAmB,EAAU,CAAQ,EAAG,CAAc,CACxE,EAEA,OAAU,EAAkB,EAAiC,CACzD,OAAO,EAAO,IAAO,EAAU,EAAU,CAAE,CAAC,CAChD,EAEA,QAAW,EAAkB,EAAwC,CACjE,OAAO,QAAQ,IAAI,EAAI,IAAK,GAAO,EAAS,OAAU,EAAU,CAAE,CAAC,CAAC,CACxE,EAEA,OAAU,EAAkB,EAA2B,CACnD,OAAO,EAAO,KAAQ,EAAU,CAAQ,EAAG,CAAE,KAAM,CAAK,CAAC,CAC7D,EAEA,OAAU,EAAkB,EAAqB,EAA2B,CACxE,IAAM,EAAO,EAAU,EAAU,CAAE,EACnC,OAAO,IAAiB,MAClB,EAAO,IAAO,EAAM,CAAE,KAAM,CAAK,CAAC,EAClC,EAAO,MAAS,EAAM,CAAE,KAAM,CAAK,CAAC,CAC9C,EAEA,UAAa,EAAkB,EAAiC,CAC5D,OAAO,EAAO,OAAU,EAAU,EAAU,CAAE,CAAC,CACnD,CACJ,EAEA,OAAO,CACX"}
|
|
1
|
+
{"version":3,"file":"create-data-provider.cjs","names":[],"sources":["../../src/data/create-data-provider.ts"],"sourcesContent":["import type { ApiClient, RequestOptions } from \"@/http/types\";\nimport type { OffsetPage } from \"@/query/pagination\";\n\n/** Query-param values accepted by the API client for a list request. */\ntype ParamValue = string | number | boolean | undefined | null;\n\n/** Filters passed to {@link DataProvider.getList}, spread verbatim into the query string. */\nexport type DataFilters = Record<string, ParamValue>;\n\n/** Parameters for a paginated, sorted, filtered list request. */\nexport interface GetListParams {\n /** Offset pagination — 1-based `page` and `pageSize`. */\n pagination?: { page?: number; pageSize?: number };\n /** Single-field sort. `order` defaults to `\"asc\"`. */\n sort?: { field: string; order?: \"asc\" | \"desc\" };\n /** Arbitrary filter query params, spread onto the request. */\n filters?: DataFilters;\n}\n\n/**\n * Refine-style data provider over the Tempest FastAPI SDK CRUD/pagination\n * conventions. Implementations are stateless wrappers around an {@link ApiClient}.\n */\nexport interface DataProvider {\n /**\n * Fetch a paginated list of a resource.\n *\n * @param resource - The resource name (e.g. `\"users\"`).\n * @param params - Pagination, sort and filter parameters.\n * @returns The offset-paginated envelope.\n */\n getList<T>(resource: string, params?: GetListParams): Promise<OffsetPage<T>>;\n /**\n * Fetch a single record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The record.\n */\n getOne<T>(resource: string, id: string | number): Promise<T>;\n /**\n * Fetch many records by id (default: parallel {@link DataProvider.getOne}).\n *\n * @param resource - The resource name.\n * @param ids - The record ids.\n * @returns The records, in the same order as `ids`.\n */\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]>;\n /**\n * Create a record.\n *\n * @param resource - The resource name.\n * @param data - The creation payload.\n * @returns The created record.\n */\n create<T>(resource: string, data: unknown): Promise<T>;\n /**\n * Update a record (PATCH by default, PUT when configured).\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @param data - The update payload.\n * @returns The updated record.\n */\n update<T>(resource: string, id: string | number, data: unknown): Promise<T>;\n /**\n * Delete a record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The delete response (often the deleted record).\n */\n deleteOne<T>(resource: string, id: string | number): Promise<T>;\n}\n\n/** Options to tailor {@link createDataProvider} to a backend's conventions. */\nexport interface DataProviderOptions {\n /** Query-param name for the page number. Default: `\"page\"`. */\n pageParam?: string;\n /** Query-param name for the page size. Default: `\"size\"`. */\n sizeParam?: string;\n /** Query-param name for the sort field. Default: `\"order_by\"`. */\n sortFieldParam?: string;\n /** Query-param name for the sort order. Default: `\"ascending\"`. */\n sortOrderParam?: string;\n /**\n * When `true` (default), emit the sort order as a boolean\n * (`order:\"asc\"` → `true`). When `false`, emit the literal `\"asc\"`/`\"desc\"`.\n */\n sortOrderAsBoolean?: boolean;\n /** HTTP method used by {@link DataProvider.update}. Default: `\"patch\"`. */\n updateMethod?: \"patch\" | \"put\";\n /**\n * Build the request path for a resource (and optional id).\n * Default: `id == null ? \"/\" + resource : \"/\" + resource + \"/\" + id`.\n *\n * Since 0.77.0 `id` arrives already passed through {@link encodeIdSegment}:\n * percent-encoded as one path segment, and never `\"\"`, `\".\"` or `\"..\"`. An\n * override therefore interpolates it as-is — encoding it again would turn\n * `%2F` into `%252F`.\n */\n buildPath?: (resource: string, id?: string | number) => string;\n}\n\nconst DOT_SEGMENTS: ReadonlySet<string> = new Set([\"\", \".\", \"..\"]);\n\n/**\n * Encode a record id as exactly one URL path segment.\n *\n * The data provider calls this on every id before building a path, because the\n * id is the one part of the URL that may come from outside the app — a search\n * param, a hash, a decoded route param, a value another user stored. Left raw,\n * `\"../../admin/users\"` walks the authenticated request out of the resource,\n * `\"7?role=admin\"` injects query params and `\"7#x\"` truncates the path, all\n * while the request still carries the bearer token (#438).\n *\n * `encodeURIComponent` neutralises `/`, `?`, `#` and `%`. It leaves `.` alone,\n * so `\"\"`, `\".\"` and `\"..\"` are rejected instead: the URL parser resolves them\n * as dot-segments whatever the encoding, and none of them names a record.\n * `null` and `undefined` — what an untyped caller or a not-yet-loaded route\n * param hands over — are rejected too, since `String()` would turn them into\n * the literal segments `\"null\"` and `\"undefined\"`.\n *\n * @param id - The record id, as handed to the provider.\n * @returns The percent-encoded segment, safe to interpolate between two `/`.\n * @throws {TypeError} When the id is `null`, `undefined`, `\"\"`, `\".\"` or `\"..\"`.\n *\n * @example\n * encodeIdSegment(7); // \"7\"\n * encodeIdSegment(\"a/b?x=1\"); // \"a%2Fb%3Fx%3D1\"\n * encodeIdSegment(\"..\"); // throws TypeError\n */\nexport function encodeIdSegment(id: string | number): string {\n if (id == null) {\n throw new TypeError(`Invalid record id ${String(id)}: a record id is required.`);\n }\n const raw = String(id);\n if (DOT_SEGMENTS.has(raw)) {\n throw new TypeError(\n `Invalid record id ${JSON.stringify(raw)}: it would resolve as a path dot-segment.`,\n );\n }\n return encodeURIComponent(raw);\n}\n\nconst defaultBuildPath = (resource: string, id?: string | number): string =>\n id == null ? `/${resource}` : `/${resource}/${id}`;\n\n/**\n * Create a {@link DataProvider} bound to an {@link ApiClient}.\n *\n * Maps Refine-style calls to the Tempest FastAPI SDK conventions: list →\n * `GET /{resource}?page=&size=&order_by=&ascending=&...filters`, one →\n * `GET /{resource}/{id}`, create → `POST`, update → `PATCH`/`PUT`,\n * delete → `DELETE`.\n *\n * Every id goes through {@link encodeIdSegment} before reaching `buildPath`, so\n * an id can never leave its own path segment — not with the default builder,\n * and not with an override that interpolates it.\n *\n * @param client - The HTTP client created by `createApiClient`.\n * @param options - Optional overrides for param names, sort encoding,\n * update method and path building.\n * @returns A stateless data provider.\n *\n * @example\n * const dataProvider = createDataProvider(apiClient);\n * const page = await dataProvider.getList<User>(\"users\", {\n * pagination: { page: 1, pageSize: 20 },\n * sort: { field: \"created_at\", order: \"desc\" },\n * filters: { active: true },\n * });\n */\nexport function createDataProvider(\n client: ApiClient,\n options: DataProviderOptions = {},\n): DataProvider {\n const {\n pageParam = \"page\",\n sizeParam = \"size\",\n sortFieldParam = \"order_by\",\n sortOrderParam = \"ascending\",\n sortOrderAsBoolean = true,\n updateMethod = \"patch\",\n buildPath = defaultBuildPath,\n } = options;\n\n const provider: DataProvider = {\n getList<T>(resource: string, params: GetListParams = {}): Promise<OffsetPage<T>> {\n const { pagination, sort, filters } = params;\n const query: Record<string, ParamValue> = {};\n\n if (pagination?.page != null) query[pageParam] = pagination.page;\n if (pagination?.pageSize != null) query[sizeParam] = pagination.pageSize;\n\n if (sort?.field) {\n const order = sort.order ?? \"asc\";\n query[sortFieldParam] = sort.field;\n query[sortOrderParam] = sortOrderAsBoolean ? order === \"asc\" : order;\n }\n\n if (filters) {\n for (const [key, value] of Object.entries(filters)) {\n query[key] = value;\n }\n }\n\n const requestOptions: RequestOptions = { params: query };\n return client.get<OffsetPage<T>>(buildPath(resource), requestOptions);\n },\n\n async getOne<T>(resource: string, id: string | number): Promise<T> {\n return client.get<T>(buildPath(resource, encodeIdSegment(id)));\n },\n\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]> {\n return Promise.all(ids.map((id) => provider.getOne<T>(resource, id)));\n },\n\n create<T>(resource: string, data: unknown): Promise<T> {\n return client.post<T>(buildPath(resource), { body: data });\n },\n\n async update<T>(resource: string, id: string | number, data: unknown): Promise<T> {\n const path = buildPath(resource, encodeIdSegment(id));\n return updateMethod === \"put\"\n ? client.put<T>(path, { body: data })\n : client.patch<T>(path, { body: data });\n },\n\n async deleteOne<T>(resource: string, id: string | number): Promise<T> {\n return client.delete<T>(buildPath(resource, encodeIdSegment(id)));\n },\n };\n\n return provider;\n}\n"],"mappings":"AAwGA,IAAM,EAAoC,IAAI,IAAI,CAAC,GAAI,IAAK,IAAI,CAAC,EA4BjE,SAAgB,EAAgB,EAA6B,CACzD,GAAI,GAAM,KACN,MAAU,UAAU,qBAAqB,OAAO,CAAE,EAAE,2BAA2B,EAEnF,IAAM,EAAM,OAAO,CAAE,EACrB,GAAI,EAAa,IAAI,CAAG,EACpB,MAAU,UACN,qBAAqB,KAAK,UAAU,CAAG,EAAE,0CAC7C,EAEJ,OAAO,mBAAmB,CAAG,CACjC,CAEA,IAAM,GAAoB,EAAkB,IACxC,GAAM,KAAO,IAAI,IAAa,IAAI,EAAS,GAAG,IA2BlD,SAAgB,EACZ,EACA,EAA+B,CAAC,EACpB,CACZ,GAAM,CACF,YAAY,OACZ,YAAY,OACZ,iBAAiB,WACjB,iBAAiB,YACjB,qBAAqB,GACrB,eAAe,QACf,YAAY,GACZ,EAEE,EAAyB,CAC3B,QAAW,EAAkB,EAAwB,CAAC,EAA2B,CAC7E,GAAM,CAAE,aAAY,OAAM,WAAY,EAChC,EAAoC,CAAC,EAK3C,GAHI,GAAY,MAAQ,OAAM,EAAM,GAAa,EAAW,MACxD,GAAY,UAAY,OAAM,EAAM,GAAa,EAAW,UAE5D,GAAM,MAAO,CACb,IAAM,EAAQ,EAAK,OAAS,MAC5B,EAAM,GAAkB,EAAK,MAC7B,EAAM,GAAkB,EAAqB,IAAU,MAAQ,CACnE,CAEA,GAAI,EACA,IAAK,GAAM,CAAC,EAAK,KAAU,OAAO,QAAQ,CAAO,EAC7C,EAAM,GAAO,EAIrB,IAAM,EAAiC,CAAE,OAAQ,CAAM,EACvD,OAAO,EAAO,IAAmB,EAAU,CAAQ,EAAG,CAAc,CACxE,EAEA,MAAM,OAAU,EAAkB,EAAiC,CAC/D,OAAO,EAAO,IAAO,EAAU,EAAU,EAAgB,CAAE,CAAC,CAAC,CACjE,EAEA,QAAW,EAAkB,EAAwC,CACjE,OAAO,QAAQ,IAAI,EAAI,IAAK,GAAO,EAAS,OAAU,EAAU,CAAE,CAAC,CAAC,CACxE,EAEA,OAAU,EAAkB,EAA2B,CACnD,OAAO,EAAO,KAAQ,EAAU,CAAQ,EAAG,CAAE,KAAM,CAAK,CAAC,CAC7D,EAEA,MAAM,OAAU,EAAkB,EAAqB,EAA2B,CAC9E,IAAM,EAAO,EAAU,EAAU,EAAgB,CAAE,CAAC,EACpD,OAAO,IAAiB,MAClB,EAAO,IAAO,EAAM,CAAE,KAAM,CAAK,CAAC,EAClC,EAAO,MAAS,EAAM,CAAE,KAAM,CAAK,CAAC,CAC9C,EAEA,MAAM,UAAa,EAAkB,EAAiC,CAClE,OAAO,EAAO,OAAU,EAAU,EAAU,EAAgB,CAAE,CAAC,CAAC,CACpE,CACJ,EAEA,OAAO,CACX"}
|
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
//#region src/data/create-data-provider.ts
|
|
2
|
-
var e =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
var e = /* @__PURE__ */ new Set([
|
|
3
|
+
"",
|
|
4
|
+
".",
|
|
5
|
+
".."
|
|
6
|
+
]);
|
|
7
|
+
function t(t) {
|
|
8
|
+
if (t == null) throw TypeError(`Invalid record id ${String(t)}: a record id is required.`);
|
|
9
|
+
let n = String(t);
|
|
10
|
+
if (e.has(n)) throw TypeError(`Invalid record id ${JSON.stringify(n)}: it would resolve as a path dot-segment.`);
|
|
11
|
+
return encodeURIComponent(n);
|
|
12
|
+
}
|
|
13
|
+
var n = (e, t) => t == null ? `/${e}` : `/${e}/${t}`;
|
|
14
|
+
function r(e, r = {}) {
|
|
15
|
+
let { pageParam: i = "page", sizeParam: a = "size", sortFieldParam: o = "order_by", sortOrderParam: s = "ascending", sortOrderAsBoolean: c = !0, updateMethod: l = "patch", buildPath: u = n } = r, d = {
|
|
16
|
+
getList(t, n = {}) {
|
|
17
|
+
let { pagination: r, sort: l, filters: d } = n, f = {};
|
|
18
|
+
if (r?.page != null && (f[i] = r.page), r?.pageSize != null && (f[a] = r.pageSize), l?.field) {
|
|
19
|
+
let e = l.order ?? "asc";
|
|
20
|
+
f[o] = l.field, f[s] = c ? e === "asc" : e;
|
|
10
21
|
}
|
|
11
22
|
if (d) for (let [e, t] of Object.entries(d)) f[e] = t;
|
|
12
23
|
let p = { params: f };
|
|
13
|
-
return
|
|
24
|
+
return e.get(u(t), p);
|
|
14
25
|
},
|
|
15
|
-
getOne(
|
|
16
|
-
return
|
|
26
|
+
async getOne(n, r) {
|
|
27
|
+
return e.get(u(n, t(r)));
|
|
17
28
|
},
|
|
18
29
|
getMany(e, t) {
|
|
19
|
-
return Promise.all(t.map((t) =>
|
|
30
|
+
return Promise.all(t.map((t) => d.getOne(e, t)));
|
|
20
31
|
},
|
|
21
|
-
create(
|
|
22
|
-
return
|
|
32
|
+
create(t, n) {
|
|
33
|
+
return e.post(u(t), { body: n });
|
|
23
34
|
},
|
|
24
|
-
update(
|
|
25
|
-
let
|
|
26
|
-
return
|
|
35
|
+
async update(n, r, i) {
|
|
36
|
+
let a = u(n, t(r));
|
|
37
|
+
return l === "put" ? e.put(a, { body: i }) : e.patch(a, { body: i });
|
|
27
38
|
},
|
|
28
|
-
deleteOne(
|
|
29
|
-
return
|
|
39
|
+
async deleteOne(n, r) {
|
|
40
|
+
return e.delete(u(n, t(r)));
|
|
30
41
|
}
|
|
31
42
|
};
|
|
32
|
-
return
|
|
43
|
+
return d;
|
|
33
44
|
}
|
|
34
45
|
//#endregion
|
|
35
|
-
export {
|
|
46
|
+
export { r as createDataProvider, t as encodeIdSegment };
|
|
36
47
|
|
|
37
48
|
//# sourceMappingURL=create-data-provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-data-provider.js","names":[],"sources":["../../src/data/create-data-provider.ts"],"sourcesContent":["import type { ApiClient, RequestOptions } from \"@/http/types\";\nimport type { OffsetPage } from \"@/query/pagination\";\n\n/** Query-param values accepted by the API client for a list request. */\ntype ParamValue = string | number | boolean | undefined | null;\n\n/** Filters passed to {@link DataProvider.getList}, spread verbatim into the query string. */\nexport type DataFilters = Record<string, ParamValue>;\n\n/** Parameters for a paginated, sorted, filtered list request. */\nexport interface GetListParams {\n /** Offset pagination — 1-based `page` and `pageSize`. */\n pagination?: { page?: number; pageSize?: number };\n /** Single-field sort. `order` defaults to `\"asc\"`. */\n sort?: { field: string; order?: \"asc\" | \"desc\" };\n /** Arbitrary filter query params, spread onto the request. */\n filters?: DataFilters;\n}\n\n/**\n * Refine-style data provider over the Tempest FastAPI SDK CRUD/pagination\n * conventions. Implementations are stateless wrappers around an {@link ApiClient}.\n */\nexport interface DataProvider {\n /**\n * Fetch a paginated list of a resource.\n *\n * @param resource - The resource name (e.g. `\"users\"`).\n * @param params - Pagination, sort and filter parameters.\n * @returns The offset-paginated envelope.\n */\n getList<T>(resource: string, params?: GetListParams): Promise<OffsetPage<T>>;\n /**\n * Fetch a single record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The record.\n */\n getOne<T>(resource: string, id: string | number): Promise<T>;\n /**\n * Fetch many records by id (default: parallel {@link DataProvider.getOne}).\n *\n * @param resource - The resource name.\n * @param ids - The record ids.\n * @returns The records, in the same order as `ids`.\n */\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]>;\n /**\n * Create a record.\n *\n * @param resource - The resource name.\n * @param data - The creation payload.\n * @returns The created record.\n */\n create<T>(resource: string, data: unknown): Promise<T>;\n /**\n * Update a record (PATCH by default, PUT when configured).\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @param data - The update payload.\n * @returns The updated record.\n */\n update<T>(resource: string, id: string | number, data: unknown): Promise<T>;\n /**\n * Delete a record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The delete response (often the deleted record).\n */\n deleteOne<T>(resource: string, id: string | number): Promise<T>;\n}\n\n/** Options to tailor {@link createDataProvider} to a backend's conventions. */\nexport interface DataProviderOptions {\n /** Query-param name for the page number. Default: `\"page\"`. */\n pageParam?: string;\n /** Query-param name for the page size. Default: `\"size\"`. */\n sizeParam?: string;\n /** Query-param name for the sort field. Default: `\"order_by\"`. */\n sortFieldParam?: string;\n /** Query-param name for the sort order. Default: `\"ascending\"`. */\n sortOrderParam?: string;\n /**\n * When `true` (default), emit the sort order as a boolean\n * (`order:\"asc\"` → `true`). When `false`, emit the literal `\"asc\"`/`\"desc\"`.\n */\n sortOrderAsBoolean?: boolean;\n /** HTTP method used by {@link DataProvider.update}. Default: `\"patch\"`. */\n updateMethod?: \"patch\" | \"put\";\n /**\n * Build the request path for a resource (and optional id).\n * Default: `id == null ? \"/\" + resource : \"/\" + resource + \"/\" + id`.\n */\n buildPath?: (resource: string, id?: string | number) => string;\n}\n\nconst defaultBuildPath = (resource: string, id?: string | number): string =>\n id == null ? `/${resource}` : `/${resource}/${id}`;\n\n/**\n * Create a {@link DataProvider} bound to an {@link ApiClient}.\n *\n * Maps Refine-style calls to the Tempest FastAPI SDK conventions: list →\n * `GET /{resource}?page=&size=&order_by=&ascending=&...filters`, one →\n * `GET /{resource}/{id}`, create → `POST`, update → `PATCH`/`PUT`,\n * delete → `DELETE`.\n *\n * @param client - The HTTP client created by `createApiClient`.\n * @param options - Optional overrides for param names, sort encoding,\n * update method and path building.\n * @returns A stateless data provider.\n *\n * @example\n * const dataProvider = createDataProvider(apiClient);\n * const page = await dataProvider.getList<User>(\"users\", {\n * pagination: { page: 1, pageSize: 20 },\n * sort: { field: \"created_at\", order: \"desc\" },\n * filters: { active: true },\n * });\n */\nexport function createDataProvider(\n client: ApiClient,\n options: DataProviderOptions = {},\n): DataProvider {\n const {\n pageParam = \"page\",\n sizeParam = \"size\",\n sortFieldParam = \"order_by\",\n sortOrderParam = \"ascending\",\n sortOrderAsBoolean = true,\n updateMethod = \"patch\",\n buildPath = defaultBuildPath,\n } = options;\n\n const provider: DataProvider = {\n getList<T>(resource: string, params: GetListParams = {}): Promise<OffsetPage<T>> {\n const { pagination, sort, filters } = params;\n const query: Record<string, ParamValue> = {};\n\n if (pagination?.page != null) query[pageParam] = pagination.page;\n if (pagination?.pageSize != null) query[sizeParam] = pagination.pageSize;\n\n if (sort?.field) {\n const order = sort.order ?? \"asc\";\n query[sortFieldParam] = sort.field;\n query[sortOrderParam] = sortOrderAsBoolean ? order === \"asc\" : order;\n }\n\n if (filters) {\n for (const [key, value] of Object.entries(filters)) {\n query[key] = value;\n }\n }\n\n const requestOptions: RequestOptions = { params: query };\n return client.get<OffsetPage<T>>(buildPath(resource), requestOptions);\n },\n\n getOne<T>(resource: string, id: string | number): Promise<T> {\n return client.get<T>(buildPath(resource, id));\n },\n\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]> {\n return Promise.all(ids.map((id) => provider.getOne<T>(resource, id)));\n },\n\n create<T>(resource: string, data: unknown): Promise<T> {\n return client.post<T>(buildPath(resource), { body: data });\n },\n\n update<T>(resource: string, id: string | number, data: unknown): Promise<T> {\n const path = buildPath(resource, id);\n return updateMethod === \"put\"\n ? client.put<T>(path, { body: data })\n : client.patch<T>(path, { body: data });\n },\n\n deleteOne<T>(resource: string, id: string | number): Promise<T> {\n return client.delete<T>(buildPath(resource, id));\n },\n };\n\n return provider;\n}\n"],"mappings":";AAmGA,IAAM,KAAoB,GAAkB,MACxC,KAAM,OAAO,IAAI,MAAa,IAAI,EAAS,GAAG;AAuBlD,SAAgB,EACZ,GACA,IAA+B,CAAC,GACpB;CACZ,IAAM,EACF,eAAY,QACZ,eAAY,QACZ,oBAAiB,YACjB,oBAAiB,aACjB,wBAAqB,IACrB,kBAAe,SACf,eAAY,MACZ,GAEE,IAAyB;EAC3B,QAAW,GAAkB,IAAwB,CAAC,GAA2B;GAC7E,IAAM,EAAE,eAAY,SAAM,eAAY,GAChC,IAAoC,CAAC;GAK3C,IAHI,GAAY,QAAQ,SAAM,EAAM,KAAa,EAAW,OACxD,GAAY,YAAY,SAAM,EAAM,KAAa,EAAW,WAE5D,GAAM,OAAO;IACb,IAAM,IAAQ,EAAK,SAAS;IAE5B,AADA,EAAM,KAAkB,EAAK,OAC7B,EAAM,KAAkB,IAAqB,MAAU,QAAQ;GACnE;GAEA,IAAI,GACA,KAAK,IAAM,CAAC,GAAK,MAAU,OAAO,QAAQ,CAAO,GAC7C,EAAM,KAAO;GAIrB,IAAM,IAAiC,EAAE,QAAQ,EAAM;GACvD,OAAO,EAAO,IAAmB,EAAU,CAAQ,GAAG,CAAc;EACxE;EAEA,OAAU,GAAkB,GAAiC;GACzD,OAAO,EAAO,IAAO,EAAU,GAAU,CAAE,CAAC;EAChD;EAEA,QAAW,GAAkB,GAAwC;GACjE,OAAO,QAAQ,IAAI,EAAI,KAAK,MAAO,EAAS,OAAU,GAAU,CAAE,CAAC,CAAC;EACxE;EAEA,OAAU,GAAkB,GAA2B;GACnD,OAAO,EAAO,KAAQ,EAAU,CAAQ,GAAG,EAAE,MAAM,EAAK,CAAC;EAC7D;EAEA,OAAU,GAAkB,GAAqB,GAA2B;GACxE,IAAM,IAAO,EAAU,GAAU,CAAE;GACnC,OAAO,MAAiB,QAClB,EAAO,IAAO,GAAM,EAAE,MAAM,EAAK,CAAC,IAClC,EAAO,MAAS,GAAM,EAAE,MAAM,EAAK,CAAC;EAC9C;EAEA,UAAa,GAAkB,GAAiC;GAC5D,OAAO,EAAO,OAAU,EAAU,GAAU,CAAE,CAAC;EACnD;CACJ;CAEA,OAAO;AACX"}
|
|
1
|
+
{"version":3,"file":"create-data-provider.js","names":[],"sources":["../../src/data/create-data-provider.ts"],"sourcesContent":["import type { ApiClient, RequestOptions } from \"@/http/types\";\nimport type { OffsetPage } from \"@/query/pagination\";\n\n/** Query-param values accepted by the API client for a list request. */\ntype ParamValue = string | number | boolean | undefined | null;\n\n/** Filters passed to {@link DataProvider.getList}, spread verbatim into the query string. */\nexport type DataFilters = Record<string, ParamValue>;\n\n/** Parameters for a paginated, sorted, filtered list request. */\nexport interface GetListParams {\n /** Offset pagination — 1-based `page` and `pageSize`. */\n pagination?: { page?: number; pageSize?: number };\n /** Single-field sort. `order` defaults to `\"asc\"`. */\n sort?: { field: string; order?: \"asc\" | \"desc\" };\n /** Arbitrary filter query params, spread onto the request. */\n filters?: DataFilters;\n}\n\n/**\n * Refine-style data provider over the Tempest FastAPI SDK CRUD/pagination\n * conventions. Implementations are stateless wrappers around an {@link ApiClient}.\n */\nexport interface DataProvider {\n /**\n * Fetch a paginated list of a resource.\n *\n * @param resource - The resource name (e.g. `\"users\"`).\n * @param params - Pagination, sort and filter parameters.\n * @returns The offset-paginated envelope.\n */\n getList<T>(resource: string, params?: GetListParams): Promise<OffsetPage<T>>;\n /**\n * Fetch a single record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The record.\n */\n getOne<T>(resource: string, id: string | number): Promise<T>;\n /**\n * Fetch many records by id (default: parallel {@link DataProvider.getOne}).\n *\n * @param resource - The resource name.\n * @param ids - The record ids.\n * @returns The records, in the same order as `ids`.\n */\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]>;\n /**\n * Create a record.\n *\n * @param resource - The resource name.\n * @param data - The creation payload.\n * @returns The created record.\n */\n create<T>(resource: string, data: unknown): Promise<T>;\n /**\n * Update a record (PATCH by default, PUT when configured).\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @param data - The update payload.\n * @returns The updated record.\n */\n update<T>(resource: string, id: string | number, data: unknown): Promise<T>;\n /**\n * Delete a record by id.\n *\n * @param resource - The resource name.\n * @param id - The record id.\n * @returns The delete response (often the deleted record).\n */\n deleteOne<T>(resource: string, id: string | number): Promise<T>;\n}\n\n/** Options to tailor {@link createDataProvider} to a backend's conventions. */\nexport interface DataProviderOptions {\n /** Query-param name for the page number. Default: `\"page\"`. */\n pageParam?: string;\n /** Query-param name for the page size. Default: `\"size\"`. */\n sizeParam?: string;\n /** Query-param name for the sort field. Default: `\"order_by\"`. */\n sortFieldParam?: string;\n /** Query-param name for the sort order. Default: `\"ascending\"`. */\n sortOrderParam?: string;\n /**\n * When `true` (default), emit the sort order as a boolean\n * (`order:\"asc\"` → `true`). When `false`, emit the literal `\"asc\"`/`\"desc\"`.\n */\n sortOrderAsBoolean?: boolean;\n /** HTTP method used by {@link DataProvider.update}. Default: `\"patch\"`. */\n updateMethod?: \"patch\" | \"put\";\n /**\n * Build the request path for a resource (and optional id).\n * Default: `id == null ? \"/\" + resource : \"/\" + resource + \"/\" + id`.\n *\n * Since 0.77.0 `id` arrives already passed through {@link encodeIdSegment}:\n * percent-encoded as one path segment, and never `\"\"`, `\".\"` or `\"..\"`. An\n * override therefore interpolates it as-is — encoding it again would turn\n * `%2F` into `%252F`.\n */\n buildPath?: (resource: string, id?: string | number) => string;\n}\n\nconst DOT_SEGMENTS: ReadonlySet<string> = new Set([\"\", \".\", \"..\"]);\n\n/**\n * Encode a record id as exactly one URL path segment.\n *\n * The data provider calls this on every id before building a path, because the\n * id is the one part of the URL that may come from outside the app — a search\n * param, a hash, a decoded route param, a value another user stored. Left raw,\n * `\"../../admin/users\"` walks the authenticated request out of the resource,\n * `\"7?role=admin\"` injects query params and `\"7#x\"` truncates the path, all\n * while the request still carries the bearer token (#438).\n *\n * `encodeURIComponent` neutralises `/`, `?`, `#` and `%`. It leaves `.` alone,\n * so `\"\"`, `\".\"` and `\"..\"` are rejected instead: the URL parser resolves them\n * as dot-segments whatever the encoding, and none of them names a record.\n * `null` and `undefined` — what an untyped caller or a not-yet-loaded route\n * param hands over — are rejected too, since `String()` would turn them into\n * the literal segments `\"null\"` and `\"undefined\"`.\n *\n * @param id - The record id, as handed to the provider.\n * @returns The percent-encoded segment, safe to interpolate between two `/`.\n * @throws {TypeError} When the id is `null`, `undefined`, `\"\"`, `\".\"` or `\"..\"`.\n *\n * @example\n * encodeIdSegment(7); // \"7\"\n * encodeIdSegment(\"a/b?x=1\"); // \"a%2Fb%3Fx%3D1\"\n * encodeIdSegment(\"..\"); // throws TypeError\n */\nexport function encodeIdSegment(id: string | number): string {\n if (id == null) {\n throw new TypeError(`Invalid record id ${String(id)}: a record id is required.`);\n }\n const raw = String(id);\n if (DOT_SEGMENTS.has(raw)) {\n throw new TypeError(\n `Invalid record id ${JSON.stringify(raw)}: it would resolve as a path dot-segment.`,\n );\n }\n return encodeURIComponent(raw);\n}\n\nconst defaultBuildPath = (resource: string, id?: string | number): string =>\n id == null ? `/${resource}` : `/${resource}/${id}`;\n\n/**\n * Create a {@link DataProvider} bound to an {@link ApiClient}.\n *\n * Maps Refine-style calls to the Tempest FastAPI SDK conventions: list →\n * `GET /{resource}?page=&size=&order_by=&ascending=&...filters`, one →\n * `GET /{resource}/{id}`, create → `POST`, update → `PATCH`/`PUT`,\n * delete → `DELETE`.\n *\n * Every id goes through {@link encodeIdSegment} before reaching `buildPath`, so\n * an id can never leave its own path segment — not with the default builder,\n * and not with an override that interpolates it.\n *\n * @param client - The HTTP client created by `createApiClient`.\n * @param options - Optional overrides for param names, sort encoding,\n * update method and path building.\n * @returns A stateless data provider.\n *\n * @example\n * const dataProvider = createDataProvider(apiClient);\n * const page = await dataProvider.getList<User>(\"users\", {\n * pagination: { page: 1, pageSize: 20 },\n * sort: { field: \"created_at\", order: \"desc\" },\n * filters: { active: true },\n * });\n */\nexport function createDataProvider(\n client: ApiClient,\n options: DataProviderOptions = {},\n): DataProvider {\n const {\n pageParam = \"page\",\n sizeParam = \"size\",\n sortFieldParam = \"order_by\",\n sortOrderParam = \"ascending\",\n sortOrderAsBoolean = true,\n updateMethod = \"patch\",\n buildPath = defaultBuildPath,\n } = options;\n\n const provider: DataProvider = {\n getList<T>(resource: string, params: GetListParams = {}): Promise<OffsetPage<T>> {\n const { pagination, sort, filters } = params;\n const query: Record<string, ParamValue> = {};\n\n if (pagination?.page != null) query[pageParam] = pagination.page;\n if (pagination?.pageSize != null) query[sizeParam] = pagination.pageSize;\n\n if (sort?.field) {\n const order = sort.order ?? \"asc\";\n query[sortFieldParam] = sort.field;\n query[sortOrderParam] = sortOrderAsBoolean ? order === \"asc\" : order;\n }\n\n if (filters) {\n for (const [key, value] of Object.entries(filters)) {\n query[key] = value;\n }\n }\n\n const requestOptions: RequestOptions = { params: query };\n return client.get<OffsetPage<T>>(buildPath(resource), requestOptions);\n },\n\n async getOne<T>(resource: string, id: string | number): Promise<T> {\n return client.get<T>(buildPath(resource, encodeIdSegment(id)));\n },\n\n getMany<T>(resource: string, ids: (string | number)[]): Promise<T[]> {\n return Promise.all(ids.map((id) => provider.getOne<T>(resource, id)));\n },\n\n create<T>(resource: string, data: unknown): Promise<T> {\n return client.post<T>(buildPath(resource), { body: data });\n },\n\n async update<T>(resource: string, id: string | number, data: unknown): Promise<T> {\n const path = buildPath(resource, encodeIdSegment(id));\n return updateMethod === \"put\"\n ? client.put<T>(path, { body: data })\n : client.patch<T>(path, { body: data });\n },\n\n async deleteOne<T>(resource: string, id: string | number): Promise<T> {\n return client.delete<T>(buildPath(resource, encodeIdSegment(id)));\n },\n };\n\n return provider;\n}\n"],"mappings":";AAwGA,IAAM,oBAAoC,IAAI,IAAI;CAAC;CAAI;CAAK;AAAI,CAAC;AA4BjE,SAAgB,EAAgB,GAA6B;CACzD,IAAI,KAAM,MACN,MAAU,UAAU,qBAAqB,OAAO,CAAE,EAAE,2BAA2B;CAEnF,IAAM,IAAM,OAAO,CAAE;CACrB,IAAI,EAAa,IAAI,CAAG,GACpB,MAAU,UACN,qBAAqB,KAAK,UAAU,CAAG,EAAE,0CAC7C;CAEJ,OAAO,mBAAmB,CAAG;AACjC;AAEA,IAAM,KAAoB,GAAkB,MACxC,KAAM,OAAO,IAAI,MAAa,IAAI,EAAS,GAAG;AA2BlD,SAAgB,EACZ,GACA,IAA+B,CAAC,GACpB;CACZ,IAAM,EACF,eAAY,QACZ,eAAY,QACZ,oBAAiB,YACjB,oBAAiB,aACjB,wBAAqB,IACrB,kBAAe,SACf,eAAY,MACZ,GAEE,IAAyB;EAC3B,QAAW,GAAkB,IAAwB,CAAC,GAA2B;GAC7E,IAAM,EAAE,eAAY,SAAM,eAAY,GAChC,IAAoC,CAAC;GAK3C,IAHI,GAAY,QAAQ,SAAM,EAAM,KAAa,EAAW,OACxD,GAAY,YAAY,SAAM,EAAM,KAAa,EAAW,WAE5D,GAAM,OAAO;IACb,IAAM,IAAQ,EAAK,SAAS;IAE5B,AADA,EAAM,KAAkB,EAAK,OAC7B,EAAM,KAAkB,IAAqB,MAAU,QAAQ;GACnE;GAEA,IAAI,GACA,KAAK,IAAM,CAAC,GAAK,MAAU,OAAO,QAAQ,CAAO,GAC7C,EAAM,KAAO;GAIrB,IAAM,IAAiC,EAAE,QAAQ,EAAM;GACvD,OAAO,EAAO,IAAmB,EAAU,CAAQ,GAAG,CAAc;EACxE;EAEA,MAAM,OAAU,GAAkB,GAAiC;GAC/D,OAAO,EAAO,IAAO,EAAU,GAAU,EAAgB,CAAE,CAAC,CAAC;EACjE;EAEA,QAAW,GAAkB,GAAwC;GACjE,OAAO,QAAQ,IAAI,EAAI,KAAK,MAAO,EAAS,OAAU,GAAU,CAAE,CAAC,CAAC;EACxE;EAEA,OAAU,GAAkB,GAA2B;GACnD,OAAO,EAAO,KAAQ,EAAU,CAAQ,GAAG,EAAE,MAAM,EAAK,CAAC;EAC7D;EAEA,MAAM,OAAU,GAAkB,GAAqB,GAA2B;GAC9E,IAAM,IAAO,EAAU,GAAU,EAAgB,CAAE,CAAC;GACpD,OAAO,MAAiB,QAClB,EAAO,IAAO,GAAM,EAAE,MAAM,EAAK,CAAC,IAClC,EAAO,MAAS,GAAM,EAAE,MAAM,EAAK,CAAC;EAC9C;EAEA,MAAM,UAAa,GAAkB,GAAiC;GAClE,OAAO,EAAO,OAAU,EAAU,GAAU,EAAgB,CAAE,CAAC,CAAC;EACpE;CACJ;CAEA,OAAO;AACX"}
|
package/dist/http/api-client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./credential-scope.cjs"),r=require("./csrf.cjs"),i=require("./decode-response.cjs"),a=require("./errors.cjs"),o=require("./retry.cjs"),s=require("./timeout.cjs");var c=new Set([`GET`,`HEAD`,`OPTIONS`]);function l(e,t){return!c.has(t)||!(e instanceof a.TempestApiError)?!1:a.isRetriableStatus(e.status)}function u(e){return e?e===!0?{}:e:null}var d=15e3,f=3e5;function p(e){return typeof FormData<`u`&&e instanceof FormData}function m(e){return Math.round(performance.now()-e)}async function h(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new a.TempestApiError(a.buildApiError(e.status,n,e.headers,t))}function g(c){let g=c.fetcher??globalThis.fetch.bind(globalThis);function _(e,t){if(t)return{};let r=c.getToken?.();return r?n.isTrustedCredentialTarget(e,c.baseURL,c.trustedOrigins)?{Authorization:`Bearer ${r}`}:(n.reportSuppressedCredential(e,c.baseURL),{}):{}}async function v(e,n,i,o){let{body:l,params:u,headers:m,signal:h,timeout:v,...y}=n,b=p(l),x=b?c.uploadTimeout:c.timeout,S=v===void 0?x===void 0?b?f:d:x:v,C=t.buildApiUrl(c.baseURL,e,{prefix:c.prefix,params:u}),w=m,T={...b?{}:{"Content-Type":`application/json`},...i?{"X-Request-ID":i}:{},...c.headers,..._(C,o),...r.csrfHeaders({method:y.method??`GET`,url:C,reference:c.baseURL,csrf:c.csrf,trustedOrigins:c.trustedOrigins,headers:{...c.headers,...w}}),...w},E=s.withTimeout(h,S),D={...y,signal:E.signal,headers:T,credentials:c.withCredentials?`include`:y.credentials,body:l==null?void 0:b?l:JSON.stringify(l)};try{return await g(C,D)}catch(e){throw E.timedOut()?new a.TempestApiError({status:0,detail:`A requisição excedeu ${S}ms e foi abandonada.`}):e}finally{E.dispose()}}async function y(e,t,n,r,i){let a=c.logger;if(!a)return v(e,t,n,i);let o=performance.now();try{let s=await v(e,t,n,i),c={requestId:n,status:s.status,ms:m(o)},l=`${r} ${e} → ${s.status}`;return s.status>=400?a.warn(l,c):a.debug(l,c),s}catch(t){throw a.warn(`${r} ${e} → no response`,{requestId:n,ms:m(o),error:t}),t}}async function b(e,t){if(c.onUnauthorized){c.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await c.onUnauthorized(e)}catch(n){c.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}async function
|
|
1
|
+
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./credential-scope.cjs"),r=require("./csrf.cjs"),i=require("./decode-response.cjs"),a=require("./errors.cjs"),o=require("./retry.cjs"),s=require("./timeout.cjs");var c=new Set([`GET`,`HEAD`,`OPTIONS`]);function l(e,t){return!c.has(t)||!(e instanceof a.TempestApiError)?!1:a.isRetriableStatus(e.status)}function u(e){return e?e===!0?{}:e:null}var d=15e3,f=3e5;function p(e){return typeof FormData<`u`&&e instanceof FormData}function m(e){return Math.round(performance.now()-e)}async function h(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new a.TempestApiError(a.buildApiError(e.status,n,e.headers,t))}function g(c){let g=c.fetcher??globalThis.fetch.bind(globalThis);function _(e,t){if(t)return{};let r=c.getToken?.();return r?n.isTrustedCredentialTarget(e,c.baseURL,c.trustedOrigins)?{Authorization:`Bearer ${r}`}:(n.reportSuppressedCredential(e,c.baseURL),{}):{}}async function v(e,n,i,o){let{body:l,params:u,headers:m,signal:h,timeout:v,...y}=n,b=p(l),x=b?c.uploadTimeout:c.timeout,S=v===void 0?x===void 0?b?f:d:x:v,C=t.buildApiUrl(c.baseURL,e,{prefix:c.prefix,params:u}),w=m,T={...b?{}:{"Content-Type":`application/json`},...i?{"X-Request-ID":i}:{},...c.headers,..._(C,o),...r.csrfHeaders({method:y.method??`GET`,url:C,reference:c.baseURL,csrf:c.csrf,trustedOrigins:c.trustedOrigins,headers:{...c.headers,...w}}),...w},E=s.withTimeout(h,S),D={...y,signal:E.signal,headers:T,credentials:c.withCredentials?`include`:y.credentials,body:l==null?void 0:b?l:JSON.stringify(l)};try{return await g(C,D)}catch(e){throw E.timedOut()?new a.TempestApiError({status:0,detail:`A requisição excedeu ${S}ms e foi abandonada.`}):e}finally{E.dispose()}}async function y(e,t,n,r,i){let a=c.logger;if(!a)return v(e,t,n,i);let o=performance.now();try{let s=await v(e,t,n,i),c={requestId:n,status:s.status,ms:m(o)},l=`${r} ${e} → ${s.status}`;return s.status>=400?a.warn(l,c):a.debug(l,c),s}catch(t){throw a.warn(`${r} ${e} → no response`,{requestId:n,ms:m(o),error:t}),t}}async function b(e,t,n){if(c.onUnauthorized){c.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await c.onUnauthorized(e,n)}catch(n){if(n instanceof a.SessionSupersededError)throw n;c.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}function x(e){return{token:e?null:c.getToken?.()??null,epoch:c.getSessionEpoch?.()??null}}async function S(t,n,r){let{skipAuth:i=!1,skipAuthRetry:o=!1,...s}=n,l=c.requestId?c.requestId():e.randomId(),u=(s.method??`GET`).toUpperCase(),d=x(i),f=await y(t,s,l,u,i);if(f.status===401&&!i&&!o){if(c.refresh){let e;try{await c.refresh(d),e=x(i),f=await y(t,s,l,u,i)}catch(e){throw e instanceof a.SessionSupersededError?e:(await b(f,l,d),await h(f,l))}f.status===401&&await b(f,l,e)}else await b(f,l,d)}if(!f.ok)throw await h(f,l);return r(f)}async function C(e,t,n){let{retry:r,...i}=t,a=u(r===void 0?c.retry:r);if(!a)return S(e,i,n);let s=(i.method??`GET`).toUpperCase();return o.retry(()=>S(e,i,n),{...a,shouldRetry:a.shouldRetry??(e=>l(e,s))})}async function w(e,t={}){return C(e,t,i.decodeByContentType)}async function T(e,t,n=`POST`,r){return w(e,{...r,method:n,body:t})}return{request:w,get:(e,t)=>w(e,{...t,method:`GET`}),post:(e,t)=>w(e,{...t,method:`POST`}),put:(e,t)=>w(e,{...t,method:`PUT`}),patch:(e,t)=>w(e,{...t,method:`PATCH`}),delete:(e,t)=>w(e,{...t,method:`DELETE`}),blob:(e,t)=>C(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>C(e,{...t},e=>e.arrayBuffer()),upload:T}}exports.createApiClient=g;
|
|
2
2
|
//# sourceMappingURL=api-client.cjs.map
|