tempest-react-sdk 0.64.0 → 0.65.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 +4 -4
- package/dist/components/Command/Command.module.cjs.map +1 -1
- package/dist/components/Command/Command.module.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.module.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.module.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +1 -0
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/use-dev-warnings.cjs +1 -1
- package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -1
- package/dist/components/DataTable/use-dev-warnings.js +4 -3
- package/dist/components/DataTable/use-dev-warnings.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.module.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.module.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js +62 -37
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.module.cjs +1 -1
- package/dist/components/InstallBanner/InstallBanner.module.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.module.js +4 -3
- package/dist/components/InstallBanner/InstallBanner.module.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.cjs +1 -1
- package/dist/components/InstallButton/InstallButton.cjs.map +1 -1
- package/dist/components/InstallButton/InstallButton.js +34 -10
- package/dist/components/InstallButton/InstallButton.js.map +1 -1
- package/dist/components/InstallButton/InstallButton.module.cjs +3 -0
- package/dist/components/InstallButton/InstallButton.module.cjs.map +1 -0
- package/dist/components/InstallButton/InstallButton.module.js +10 -0
- package/dist/components/InstallButton/InstallButton.module.js.map +1 -0
- package/dist/components/InstallButton/install-hints.cjs +2 -0
- package/dist/components/InstallButton/install-hints.cjs.map +1 -0
- package/dist/components/InstallButton/install-hints.js +32 -0
- package/dist/components/InstallButton/install-hints.js.map +1 -0
- package/dist/components/SegmentedControl/SegmentedControl.module.cjs.map +1 -1
- package/dist/components/SegmentedControl/SegmentedControl.module.js.map +1 -1
- package/dist/components/Select/Select.cjs +1 -1
- package/dist/components/Select/Select.cjs.map +1 -1
- package/dist/components/Select/Select.js +43 -40
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/Select.module.cjs +1 -1
- package/dist/components/Select/Select.module.cjs.map +1 -1
- package/dist/components/Select/Select.module.js +5 -3
- package/dist/components/Select/Select.module.js.map +1 -1
- package/dist/hooks/use-install-prompt.cjs +1 -1
- package/dist/hooks/use-install-prompt.cjs.map +1 -1
- package/dist/hooks/use-install-prompt.js +9 -2
- package/dist/hooks/use-install-prompt.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 +80 -74
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/decode-response.cjs +2 -0
- package/dist/http/decode-response.cjs.map +1 -0
- package/dist/http/decode-response.js +28 -0
- package/dist/http/decode-response.js.map +1 -0
- package/dist/styles/InstallButton.css +2 -0
- package/dist/styles/actions.css +6 -1
- package/dist/styles/advanced.css +2 -2
- package/dist/styles/component/Command.css +1 -1
- package/dist/styles/component/ContextMenu.css +1 -1
- package/dist/styles/component/DropdownMenu.css +1 -1
- package/dist/styles/component/InstallBanner.css +2 -0
- package/dist/styles/component/InstallButton.css +4 -0
- package/dist/styles/component/SegmentedControl.css +1 -1
- package/dist/styles/component/Select.css +4 -0
- package/dist/styles/core.css +2 -2
- package/dist/styles/forms.css +4 -0
- package/dist/styles/manifest.json +2 -1
- package/dist/styles/navigation.css +1 -1
- package/dist/styles/tokens.css +2 -2
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +258 -26
- package/dist/tempest-react-sdk.js +319 -318
- package/dist/theme/ThemeProvider.cjs +1 -1
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js +40 -45
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/create-theme.cjs +1 -1
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js +1 -1
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/initial-theme.cjs +2 -2
- package/dist/theme/initial-theme.cjs.map +1 -1
- package/dist/theme/initial-theme.js +7 -12
- package/dist/theme/initial-theme.js.map +1 -1
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +5 -3
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/storage.cjs +1 -1
- package/dist/utils/storage.cjs.map +1 -1
- package/dist/utils/storage.js +13 -0
- package/dist/utils/storage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("./Select.module.cjs");let n=require("react"),r=require("react/jsx-runtime");var i=(0,n.forwardRef)(function(
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./Select.module.cjs");let n=require("react"),r=require("react/jsx-runtime");var i=(0,n.forwardRef)(function(i,o){let{options:s,placeholder:c,wrapperClassName:l,className:u,children:d,id:f,required:p,variant:m=`field`,label:h,helperText:g,error:_,...v}=i,y=m===`chip`,b=(0,n.useId)(),x=f??b,S=(0,r.jsxs)(`div`,{className:e.cn(t.default.field,y&&t.default.chip),children:[(0,r.jsxs)(`select`,{ref:o,id:x,"aria-invalid":!!_,required:p,className:e.cn(t.default.select,u),...v,children:[c&&(0,r.jsx)(`option`,{value:``,disabled:!0,hidden:!0,children:c}),s?.map(e=>(0,r.jsx)(`option`,{value:e.value,disabled:e.disabled,children:e.label},e.value)),d]}),(0,r.jsx)(`span`,{className:t.default.caret,"aria-hidden":!0,children:(0,r.jsx)(a,{})})]});return y?(0,r.jsx)(`div`,{className:e.cn(t.default.chipWrapper,l),children:S}):(0,r.jsxs)(`div`,{className:e.cn(t.default.wrapper,_&&t.default.error,l),children:[h&&(0,r.jsxs)(`label`,{htmlFor:x,className:t.default.label,children:[h,p&&(0,r.jsx)(`span`,{className:t.default.required,children:`*`})]}),S,_?(0,r.jsx)(`span`,{className:t.default.errorText,children:_}):g?(0,r.jsx)(`span`,{className:t.default.helper,children:g}):null]})});function a(){return(0,r.jsx)(`svg`,{width:`14`,height:`14`,viewBox:`0 0 24 24`,fill:`none`,children:(0,r.jsx)(`path`,{d:`M6 9l6 6 6-6`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})})}exports.Select=i;
|
|
2
2
|
//# sourceMappingURL=Select.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.cjs","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\nexport interface
|
|
1
|
+
{"version":3,"file":"Select.cjs","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\n/** What both variants of {@link Select} accept. */\nexport interface SelectBaseProps extends SelectHTMLAttributes<HTMLSelectElement> {\n options?: SelectOption[];\n placeholder?: string;\n wrapperClassName?: string;\n}\n\n/** The full form field: label, control, helper text and error slot. */\nexport interface SelectFieldProps extends SelectBaseProps {\n variant?: \"field\";\n label?: string;\n helperText?: string;\n error?: string;\n}\n\n/**\n * The bare control, sized to its content.\n *\n * `aria-label` is required rather than optional, because the variant removes the\n * only visible label the component had. A settings row names the control in the\n * row's own text, which the `<select>` has no relationship to — so without this\n * the screen reader announces a combobox with no name at all.\n */\nexport interface SelectChipProps extends SelectBaseProps {\n variant: \"chip\";\n \"aria-label\": string;\n}\n\nexport type SelectProps = SelectFieldProps | SelectChipProps;\n\n/**\n * Native `<select>` wrapper. Either provide `options` for a quick render, or\n * pass `<option>` children directly.\n *\n * Two variants:\n *\n * - `\"field\"` (default) — label, control, helper text and error slot, for a form.\n * - `\"chip\"` — the control alone, sized to its content, for a settings row where\n * the icon and the label are already on the left and the control belongs on the\n * right. Rendering the full field there stacks two labelled fields inside one\n * list item, which is why every app ended up hand-rolling a native `<select>`\n * with `appearance: none` — and hand-rolling it loses the focus ring, the\n * disabled styling and the caret this component already solves.\n *\n * @example\n * <Select label=\"Estado\" options={states} placeholder=\"Selecione\" />\n *\n * @example\n * <ListTile title=\"Idioma\" trailing={<Select variant=\"chip\" aria-label=\"Idioma\" options={langs} />} />\n */\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(props, ref) {\n const {\n options,\n placeholder,\n wrapperClassName,\n className,\n children,\n id,\n required,\n variant = \"field\",\n label,\n helperText,\n error,\n ...rest\n } = props as SelectBaseProps & {\n variant?: \"field\" | \"chip\";\n label?: string;\n helperText?: string;\n error?: string;\n };\n const isChip = variant === \"chip\";\n const generatedId = useId();\n const selectId = id ?? generatedId;\n\n const control = (\n <div className={cn(styles.field, isChip && styles.chip)}>\n <select\n ref={ref}\n id={selectId}\n aria-invalid={!!error}\n required={required}\n className={cn(styles.select, className)}\n {...rest}\n >\n {placeholder && (\n <option value=\"\" disabled hidden>\n {placeholder}\n </option>\n )}\n {options?.map((opt) => (\n <option key={opt.value} value={opt.value} disabled={opt.disabled}>\n {opt.label}\n </option>\n ))}\n {children}\n </select>\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n );\n\n if (isChip) {\n return <div className={cn(styles.chipWrapper, wrapperClassName)}>{control}</div>;\n }\n\n return (\n <div className={cn(styles.wrapper, error && styles.error, wrapperClassName)}>\n {label && (\n <label htmlFor={selectId} className={styles.label}>\n {label}\n {required && <span className={styles.required}>*</span>}\n </label>\n )}\n {control}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n});\n\nfunction CaretIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":"6HA6DA,IAAa,GAAA,EAAS,EAAA,WAAA,CAA2C,SAAgB,EAAO,EAAK,CACzF,GAAM,CACF,UACA,cACA,mBACA,YACA,WACA,KACA,WACA,UAAU,QACV,QACA,aACA,QACA,GAAG,GACH,EAME,EAAS,IAAY,OACrB,GAAA,EAAc,EAAA,MAAA,CAAM,EACpB,EAAW,GAAM,EAEjB,GACF,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,MAAO,GAAU,EAAA,QAAO,IAAI,EAAtD,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACS,MACL,GAAI,EACJ,eAAc,CAAC,CAAC,EACN,WACV,UAAW,EAAA,GAAG,EAAA,QAAO,OAAQ,CAAS,EACtC,GAAI,EANR,SAAA,CAQK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,GAAG,SAAA,GAAS,OAAA,GACrB,SAAA,CACG,CAAA,EAEX,GAAS,IAAK,IACX,EAAA,EAAA,IAAA,CAAC,SAAD,CAAwB,MAAO,EAAI,MAAO,SAAU,EAAI,SACnD,SAAA,EAAI,KACD,EAFK,EAAI,KAET,CACX,EACA,CACG,CACR,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAO,cAAA,GAC3B,UAAA,EAAA,EAAA,IAAA,CAAC,EAAD,CAAY,CAAA,CACV,CAAA,CACL,IAOT,OAJI,GACO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,YAAa,CAAgB,EAAI,SAAA,CAAa,CAAA,GAI/E,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,QAAS,GAAS,EAAA,QAAO,MAAO,CAAgB,EAA1E,SAAA,CACK,IACG,EAAA,EAAA,KAAA,CAAC,QAAD,CAAO,QAAS,EAAU,UAAW,EAAA,QAAO,MAA5C,SAAA,CACK,EACA,IAAY,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,SAAU,SAAA,GAAO,CAAA,CACnD,IAEV,EACA,GACG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,UAAY,SAAA,CAAY,CAAA,EAChD,GACA,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,OAAS,SAAA,CAAiB,CAAA,EAClD,IACH,GAEb,CAAC,EAED,SAAS,GAAY,CACjB,OACI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,YAAY,KAAK,OACjD,UAAA,EAAA,EAAA,IAAA,CAAC,OAAD,CACI,EAAE,eACF,OAAO,eACP,YAAY,IACZ,cAAc,QACd,eAAe,OAClB,CAAA,CACA,CAAA,CAEb"}
|
|
@@ -3,54 +3,57 @@ import t from "./Select.module.js";
|
|
|
3
3
|
import { forwardRef as n, useId as r } from "react";
|
|
4
4
|
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
5
5
|
//#region src/components/Select/Select.tsx
|
|
6
|
-
var o = n(function(
|
|
7
|
-
let v = r(),
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
var o = n(function(n, o) {
|
|
7
|
+
let { options: c, placeholder: l, wrapperClassName: u, className: d, children: f, id: p, required: m, variant: h = "field", label: g, helperText: _, error: v, ...y } = n, b = h === "chip", x = r(), S = p ?? x, C = /* @__PURE__ */ a("div", {
|
|
8
|
+
className: e(t.field, b && t.chip),
|
|
9
|
+
children: [/* @__PURE__ */ a("select", {
|
|
10
|
+
ref: o,
|
|
11
|
+
id: S,
|
|
12
|
+
"aria-invalid": !!v,
|
|
13
|
+
required: m,
|
|
14
|
+
className: e(t.select, d),
|
|
15
|
+
...y,
|
|
16
|
+
children: [
|
|
17
|
+
l && /* @__PURE__ */ i("option", {
|
|
18
|
+
value: "",
|
|
19
|
+
disabled: !0,
|
|
20
|
+
hidden: !0,
|
|
21
|
+
children: l
|
|
22
|
+
}),
|
|
23
|
+
c?.map((e) => /* @__PURE__ */ i("option", {
|
|
24
|
+
value: e.value,
|
|
25
|
+
disabled: e.disabled,
|
|
26
|
+
children: e.label
|
|
27
|
+
}, e.value)),
|
|
28
|
+
f
|
|
29
|
+
]
|
|
30
|
+
}), /* @__PURE__ */ i("span", {
|
|
31
|
+
className: t.caret,
|
|
32
|
+
"aria-hidden": !0,
|
|
33
|
+
children: /* @__PURE__ */ i(s, {})
|
|
34
|
+
})]
|
|
35
|
+
});
|
|
36
|
+
return b ? /* @__PURE__ */ i("div", {
|
|
37
|
+
className: e(t.chipWrapper, u),
|
|
38
|
+
children: C
|
|
39
|
+
}) : /* @__PURE__ */ a("div", {
|
|
40
|
+
className: e(t.wrapper, v && t.error, u),
|
|
10
41
|
children: [
|
|
11
|
-
|
|
12
|
-
htmlFor:
|
|
42
|
+
g && /* @__PURE__ */ a("label", {
|
|
43
|
+
htmlFor: S,
|
|
13
44
|
className: t.label,
|
|
14
|
-
children: [
|
|
45
|
+
children: [g, m && /* @__PURE__ */ i("span", {
|
|
15
46
|
className: t.required,
|
|
16
47
|
children: "*"
|
|
17
48
|
})]
|
|
18
49
|
}),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
children: [/* @__PURE__ */ a("select", {
|
|
22
|
-
ref: _,
|
|
23
|
-
id: y,
|
|
24
|
-
"aria-invalid": !!c,
|
|
25
|
-
required: h,
|
|
26
|
-
className: e(t.select, f),
|
|
27
|
-
...g,
|
|
28
|
-
children: [
|
|
29
|
-
u && /* @__PURE__ */ i("option", {
|
|
30
|
-
value: "",
|
|
31
|
-
disabled: !0,
|
|
32
|
-
hidden: !0,
|
|
33
|
-
children: u
|
|
34
|
-
}),
|
|
35
|
-
l?.map((e) => /* @__PURE__ */ i("option", {
|
|
36
|
-
value: e.value,
|
|
37
|
-
disabled: e.disabled,
|
|
38
|
-
children: e.label
|
|
39
|
-
}, e.value)),
|
|
40
|
-
p
|
|
41
|
-
]
|
|
42
|
-
}), /* @__PURE__ */ i("span", {
|
|
43
|
-
className: t.caret,
|
|
44
|
-
"aria-hidden": !0,
|
|
45
|
-
children: /* @__PURE__ */ i(s, {})
|
|
46
|
-
})]
|
|
47
|
-
}),
|
|
48
|
-
c ? /* @__PURE__ */ i("span", {
|
|
50
|
+
C,
|
|
51
|
+
v ? /* @__PURE__ */ i("span", {
|
|
49
52
|
className: t.errorText,
|
|
50
|
-
children:
|
|
51
|
-
}) :
|
|
53
|
+
children: v
|
|
54
|
+
}) : _ ? /* @__PURE__ */ i("span", {
|
|
52
55
|
className: t.helper,
|
|
53
|
-
children:
|
|
56
|
+
children: _
|
|
54
57
|
}) : null
|
|
55
58
|
]
|
|
56
59
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.js","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\nexport interface
|
|
1
|
+
{"version":3,"file":"Select.js","names":[],"sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import { forwardRef, useId } from \"react\";\nimport type { SelectHTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./Select.module.css\";\n\nexport interface SelectOption {\n value: string | number;\n label: string;\n disabled?: boolean;\n}\n\n/** What both variants of {@link Select} accept. */\nexport interface SelectBaseProps extends SelectHTMLAttributes<HTMLSelectElement> {\n options?: SelectOption[];\n placeholder?: string;\n wrapperClassName?: string;\n}\n\n/** The full form field: label, control, helper text and error slot. */\nexport interface SelectFieldProps extends SelectBaseProps {\n variant?: \"field\";\n label?: string;\n helperText?: string;\n error?: string;\n}\n\n/**\n * The bare control, sized to its content.\n *\n * `aria-label` is required rather than optional, because the variant removes the\n * only visible label the component had. A settings row names the control in the\n * row's own text, which the `<select>` has no relationship to — so without this\n * the screen reader announces a combobox with no name at all.\n */\nexport interface SelectChipProps extends SelectBaseProps {\n variant: \"chip\";\n \"aria-label\": string;\n}\n\nexport type SelectProps = SelectFieldProps | SelectChipProps;\n\n/**\n * Native `<select>` wrapper. Either provide `options` for a quick render, or\n * pass `<option>` children directly.\n *\n * Two variants:\n *\n * - `\"field\"` (default) — label, control, helper text and error slot, for a form.\n * - `\"chip\"` — the control alone, sized to its content, for a settings row where\n * the icon and the label are already on the left and the control belongs on the\n * right. Rendering the full field there stacks two labelled fields inside one\n * list item, which is why every app ended up hand-rolling a native `<select>`\n * with `appearance: none` — and hand-rolling it loses the focus ring, the\n * disabled styling and the caret this component already solves.\n *\n * @example\n * <Select label=\"Estado\" options={states} placeholder=\"Selecione\" />\n *\n * @example\n * <ListTile title=\"Idioma\" trailing={<Select variant=\"chip\" aria-label=\"Idioma\" options={langs} />} />\n */\nexport const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select(props, ref) {\n const {\n options,\n placeholder,\n wrapperClassName,\n className,\n children,\n id,\n required,\n variant = \"field\",\n label,\n helperText,\n error,\n ...rest\n } = props as SelectBaseProps & {\n variant?: \"field\" | \"chip\";\n label?: string;\n helperText?: string;\n error?: string;\n };\n const isChip = variant === \"chip\";\n const generatedId = useId();\n const selectId = id ?? generatedId;\n\n const control = (\n <div className={cn(styles.field, isChip && styles.chip)}>\n <select\n ref={ref}\n id={selectId}\n aria-invalid={!!error}\n required={required}\n className={cn(styles.select, className)}\n {...rest}\n >\n {placeholder && (\n <option value=\"\" disabled hidden>\n {placeholder}\n </option>\n )}\n {options?.map((opt) => (\n <option key={opt.value} value={opt.value} disabled={opt.disabled}>\n {opt.label}\n </option>\n ))}\n {children}\n </select>\n <span className={styles.caret} aria-hidden>\n <CaretIcon />\n </span>\n </div>\n );\n\n if (isChip) {\n return <div className={cn(styles.chipWrapper, wrapperClassName)}>{control}</div>;\n }\n\n return (\n <div className={cn(styles.wrapper, error && styles.error, wrapperClassName)}>\n {label && (\n <label htmlFor={selectId} className={styles.label}>\n {label}\n {required && <span className={styles.required}>*</span>}\n </label>\n )}\n {control}\n {error ? (\n <span className={styles.errorText}>{error}</span>\n ) : helperText ? (\n <span className={styles.helper}>{helperText}</span>\n ) : null}\n </div>\n );\n});\n\nfunction CaretIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\">\n <path\n d=\"M6 9l6 6 6-6\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n}\n"],"mappings":";;;;;AA6DA,IAAa,IAAS,EAA2C,SAAgB,GAAO,GAAK;CACzF,IAAM,EACF,YACA,gBACA,qBACA,cACA,aACA,OACA,aACA,aAAU,SACV,UACA,eACA,UACA,GAAG,MACH,GAME,IAAS,MAAY,QACrB,IAAc,EAAM,GACpB,IAAW,KAAM,GAEjB,IACF,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,OAAO,KAAU,EAAO,IAAI;EAAtD,UAAA,CACI,kBAAC,UAAD;GACS;GACL,IAAI;GACJ,gBAAc,CAAC,CAAC;GACN;GACV,WAAW,EAAG,EAAO,QAAQ,CAAS;GACtC,GAAI;GANR,UAAA;IAQK,KACG,kBAAC,UAAD;KAAQ,OAAM;KAAG,UAAA;KAAS,QAAA;KACrB,UAAA;IACG,CAAA;IAEX,GAAS,KAAK,MACX,kBAAC,UAAD;KAAwB,OAAO,EAAI;KAAO,UAAU,EAAI;KACnD,UAAA,EAAI;IACD,GAFK,EAAI,KAET,CACX;IACA;GACG;EACR,CAAA,GAAA,kBAAC,QAAD;GAAM,WAAW,EAAO;GAAO,eAAA;GAC3B,UAAA,kBAAC,GAAD,CAAY,CAAA;EACV,CAAA,CACL;;CAOT,OAJI,IACO,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,aAAa,CAAgB;EAAI,UAAA;CAAa,CAAA,IAI/E,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,SAAS,KAAS,EAAO,OAAO,CAAgB;EAA1E,UAAA;GACK,KACG,kBAAC,SAAD;IAAO,SAAS;IAAU,WAAW,EAAO;IAA5C,UAAA,CACK,GACA,KAAY,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAU,UAAA;IAAO,CAAA,CACnD;;GAEV;GACA,IACG,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAY,UAAA;GAAY,CAAA,IAChD,IACA,kBAAC,QAAD;IAAM,WAAW,EAAO;IAAS,UAAA;GAAiB,CAAA,IAClD;EACH;;AAEb,CAAC;AAED,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;GACd,gBAAe;EAClB,CAAA;CACA,CAAA;AAEb"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
require("../../styles/component/Select.css");
|
|
2
|
-
var e=`tempest_wrapper_KS2K3`,t=`tempest_label_Lmgos`,n=`tempest_required_PvDVJ`,r=`tempest_field_h-wBy`,i=`tempest_select_cjdcr`,a=`tempest_caret_MdCao`,o=`tempest_error_sw9MU`,s=`tempest_helper_frosK`,c=`tempest_errorText_-zd6i`,l={wrapper:e,label:t,required:n,field:r,select:i,caret:a,error:o,helper:s,errorText:c};exports.caret=a,exports.
|
|
2
|
+
var e=`tempest_wrapper_KS2K3`,t=`tempest_label_Lmgos`,n=`tempest_required_PvDVJ`,r=`tempest_field_h-wBy`,i=`tempest_select_cjdcr`,a=`tempest_caret_MdCao`,o=`tempest_error_sw9MU`,s=`tempest_helper_frosK`,c=`tempest_errorText_-zd6i`,l=`tempest_chipWrapper_5keyI`,u=`tempest_chip_r4NVm`,d={wrapper:e,label:t,required:n,field:r,select:i,caret:a,error:o,helper:s,errorText:c,chipWrapper:l,chip:u};exports.caret=a,exports.chip=u,exports.chipWrapper=l,exports.default=d,exports.error=o,exports.errorText=c,exports.field=r,exports.helper=s,exports.label=t,exports.required=n,exports.select=i,exports.wrapper=e;
|
|
3
3
|
//# sourceMappingURL=Select.module.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.module.cjs","names":[],"sources":["../../../src/components/Select/Select.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.required {\n color: var(--tempest-danger);\n margin-left: 2px;\n}\n\n.field {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.select {\n width: 100%;\n height: var(--tempest-control-height-md);\n padding: 0 36px 0 var(--tempest-control-padding-md);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-control-font-md);\n line-height: var(--tempest-leading-snug);\n appearance: none;\n cursor: pointer;\n box-shadow: var(--tempest-shadow-xs);\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.select:hover:not(:disabled):not(:focus) {\n border-color: var(--tempest-border-strong);\n}\n\n.select:focus {\n outline: none;\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.select:disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n color: var(--tempest-text-muted);\n box-shadow: none;\n}\n\n.caret {\n position: absolute;\n right: 12px;\n pointer-events: none;\n color: var(--tempest-text-muted);\n}\n\n.error .select {\n border-color: var(--tempest-danger);\n}\n\n.error .select:focus {\n box-shadow: 0 0 0 var(--tempest-focus-ring-width)\n color-mix(in srgb, var(--tempest-danger) 25%, transparent);\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n line-height: var(--tempest-leading-snug);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n line-height: var(--tempest-leading-snug);\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"Select.module.cjs","names":[],"sources":["../../../src/components/Select/Select.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.required {\n color: var(--tempest-danger);\n margin-left: 2px;\n}\n\n.field {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.select {\n width: 100%;\n height: var(--tempest-control-height-md);\n padding: 0 36px 0 var(--tempest-control-padding-md);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-control-font-md);\n line-height: var(--tempest-leading-snug);\n appearance: none;\n cursor: pointer;\n box-shadow: var(--tempest-shadow-xs);\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.select:hover:not(:disabled):not(:focus) {\n border-color: var(--tempest-border-strong);\n}\n\n.select:focus {\n outline: none;\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.select:disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n color: var(--tempest-text-muted);\n box-shadow: none;\n}\n\n.caret {\n position: absolute;\n right: 12px;\n pointer-events: none;\n color: var(--tempest-text-muted);\n}\n\n.error .select {\n border-color: var(--tempest-danger);\n}\n\n.error .select:focus {\n box-shadow: 0 0 0 var(--tempest-focus-ring-width)\n color-mix(in srgb, var(--tempest-danger) 25%, transparent);\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n line-height: var(--tempest-leading-snug);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n line-height: var(--tempest-leading-snug);\n}\n\n.chipWrapper {\n display: inline-flex;\n}\n\n.chip {\n width: auto;\n}\n\n.chip .select {\n width: auto;\n height: var(--tempest-control-height-sm);\n padding: 0 30px 0 var(--tempest-space-3);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n box-shadow: none;\n}\n\n.chip .caret {\n right: 10px;\n}\n"],"mappings":";"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../../styles/component/Select.css";
|
|
2
2
|
//#region src/components/Select/Select.module.css
|
|
3
|
-
var e = "tempest_wrapper_KS2K3", t = "tempest_label_Lmgos", n = "tempest_required_PvDVJ", r = "tempest_field_h-wBy", i = "tempest_select_cjdcr", a = "tempest_caret_MdCao", o = "tempest_error_sw9MU", s = "tempest_helper_frosK", c = "tempest_errorText_-zd6i", l = {
|
|
3
|
+
var e = "tempest_wrapper_KS2K3", t = "tempest_label_Lmgos", n = "tempest_required_PvDVJ", r = "tempest_field_h-wBy", i = "tempest_select_cjdcr", a = "tempest_caret_MdCao", o = "tempest_error_sw9MU", s = "tempest_helper_frosK", c = "tempest_errorText_-zd6i", l = "tempest_chipWrapper_5keyI", u = "tempest_chip_r4NVm", d = {
|
|
4
4
|
wrapper: e,
|
|
5
5
|
label: t,
|
|
6
6
|
required: n,
|
|
@@ -9,9 +9,11 @@ var e = "tempest_wrapper_KS2K3", t = "tempest_label_Lmgos", n = "tempest_require
|
|
|
9
9
|
caret: a,
|
|
10
10
|
error: o,
|
|
11
11
|
helper: s,
|
|
12
|
-
errorText: c
|
|
12
|
+
errorText: c,
|
|
13
|
+
chipWrapper: l,
|
|
14
|
+
chip: u
|
|
13
15
|
};
|
|
14
16
|
//#endregion
|
|
15
|
-
export { a as caret, l as default, o as error, c as errorText, r as field, s as helper, t as label, n as required, i as select, e as wrapper };
|
|
17
|
+
export { a as caret, u as chip, l as chipWrapper, d as default, o as error, c as errorText, r as field, s as helper, t as label, n as required, i as select, e as wrapper };
|
|
16
18
|
|
|
17
19
|
//# sourceMappingURL=Select.module.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.module.js","names":[],"sources":["../../../src/components/Select/Select.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.required {\n color: var(--tempest-danger);\n margin-left: 2px;\n}\n\n.field {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.select {\n width: 100%;\n height: var(--tempest-control-height-md);\n padding: 0 36px 0 var(--tempest-control-padding-md);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-control-font-md);\n line-height: var(--tempest-leading-snug);\n appearance: none;\n cursor: pointer;\n box-shadow: var(--tempest-shadow-xs);\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.select:hover:not(:disabled):not(:focus) {\n border-color: var(--tempest-border-strong);\n}\n\n.select:focus {\n outline: none;\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.select:disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n color: var(--tempest-text-muted);\n box-shadow: none;\n}\n\n.caret {\n position: absolute;\n right: 12px;\n pointer-events: none;\n color: var(--tempest-text-muted);\n}\n\n.error .select {\n border-color: var(--tempest-danger);\n}\n\n.error .select:focus {\n box-shadow: 0 0 0 var(--tempest-focus-ring-width)\n color-mix(in srgb, var(--tempest-danger) 25%, transparent);\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n line-height: var(--tempest-leading-snug);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n line-height: var(--tempest-leading-snug);\n}\n"],"mappings":";"}
|
|
1
|
+
{"version":3,"file":"Select.module.js","names":[],"sources":["../../../src/components/Select/Select.module.css"],"sourcesContent":[".wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--tempest-space-1);\n width: 100%;\n}\n\n.label {\n font-size: var(--tempest-text-sm);\n font-weight: var(--tempest-weight-semibold);\n color: var(--tempest-text);\n line-height: var(--tempest-leading-snug);\n}\n\n.required {\n color: var(--tempest-danger);\n margin-left: 2px;\n}\n\n.field {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.select {\n width: 100%;\n height: var(--tempest-control-height-md);\n padding: 0 36px 0 var(--tempest-control-padding-md);\n border: 1px solid var(--tempest-border);\n border-radius: var(--tempest-control-radius);\n background-color: var(--tempest-bg);\n color: var(--tempest-text);\n font-family: var(--tempest-font-sans);\n font-size: var(--tempest-control-font-md);\n line-height: var(--tempest-leading-snug);\n appearance: none;\n cursor: pointer;\n box-shadow: var(--tempest-shadow-xs);\n transition: var(--tempest-transition-color), var(--tempest-transition-shadow);\n}\n\n.select:hover:not(:disabled):not(:focus) {\n border-color: var(--tempest-border-strong);\n}\n\n.select:focus {\n outline: none;\n border-color: var(--tempest-primary);\n box-shadow: 0 0 0 var(--tempest-focus-ring-width) var(--tempest-focus-ring-color);\n}\n\n.select:disabled {\n background-color: var(--tempest-surface);\n cursor: not-allowed;\n color: var(--tempest-text-muted);\n box-shadow: none;\n}\n\n.caret {\n position: absolute;\n right: 12px;\n pointer-events: none;\n color: var(--tempest-text-muted);\n}\n\n.error .select {\n border-color: var(--tempest-danger);\n}\n\n.error .select:focus {\n box-shadow: 0 0 0 var(--tempest-focus-ring-width)\n color-mix(in srgb, var(--tempest-danger) 25%, transparent);\n}\n\n.helper {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-text-muted);\n line-height: var(--tempest-leading-snug);\n}\n\n.errorText {\n font-size: var(--tempest-text-xs);\n color: var(--tempest-danger);\n line-height: var(--tempest-leading-snug);\n}\n\n.chipWrapper {\n display: inline-flex;\n}\n\n.chip {\n width: auto;\n}\n\n.chip .select {\n width: auto;\n height: var(--tempest-control-height-sm);\n padding: 0 30px 0 var(--tempest-space-3);\n border-radius: var(--tempest-radius-full);\n background-color: var(--tempest-surface);\n font-size: var(--tempest-text-sm);\n box-shadow: none;\n}\n\n.chip .caret {\n right: 10px;\n}\n"],"mappings":";"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./pwa-env.cjs");let t=require("react");var n=6048e5,r=3e3,i=`tempest:install-declined-at`,a=null,o=new Set;typeof window<`u`&&(window.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),a=e,o.forEach(e=>e(a))}),window.addEventListener(`appinstalled`,()=>{a=null,o.forEach(e=>e(null))}));function s(e,t){if(typeof window>`u`)return!1;let n=window.localStorage.getItem(e)
|
|
1
|
+
const e=require("./pwa-env.cjs");let t=require("react");var n=6048e5,r=3e3,i=`tempest:install-declined-at`,a=null,o=new Set;typeof window<`u`&&(window.addEventListener(`beforeinstallprompt`,e=>{e.preventDefault(),a=e,o.forEach(e=>e(a))}),window.addEventListener(`appinstalled`,()=>{a=null,o.forEach(e=>e(null))}));function s(e,t){if(typeof window>`u`)return!1;let n;try{n=window.localStorage.getItem(e)}catch{return!1}if(!n)return!1;let r=Number(n);return Number.isFinite(r)?Date.now()-r<t:!1}function c(c={}){let{declineStorageKey:l=i,declineCooldownMs:u=n,manualFallbackDelayMs:d=r}=c,[f,p]=(0,t.useState)(a),[m,h]=(0,t.useState)(e.isStandalone()),[g,_]=(0,t.useState)(!1);(0,t.useEffect)(()=>(o.add(p),()=>{o.delete(p)}),[]),(0,t.useEffect)(()=>{if(typeof window>`u`)return;let t=window.matchMedia?.(`(display-mode: standalone)`),n=()=>h(e.isStandalone());return t?.addEventListener?.(`change`,n),window.addEventListener(`appinstalled`,n),()=>{t?.removeEventListener?.(`change`,n),window.removeEventListener(`appinstalled`,n)}},[]),(0,t.useEffect)(()=>{if(f){_(!1);return}let e=window.setTimeout(()=>{a||_(!0)},d);return()=>window.clearTimeout(e)},[f,d]);let v=e.isIOS(),y=e.isAndroidWithoutPromptApi(),b=s(l,u),x=`none`;!m&&!b&&(f?x=`native`:v?x=`ios`:(y||g)&&(x=`manual`));let S=()=>{if(!(typeof window>`u`))try{window.localStorage.setItem(l,String(Date.now()))}catch{}};return{deferredPrompt:f,canInstall:x!==`none`,isIOS:v,isStandalone:m,isManualAndroid:y,promptTimedOut:g,method:x,openInChromeIntent:e.buildOpenInChromeIntent(),install:async()=>{if(!f)return!1;await f.prompt();let e=await f.userChoice;return e.outcome===`dismissed`&&S(),a=null,o.forEach(e=>e(null)),e.outcome===`accepted`},recordDecline:S}}exports.useInstallPrompt=c;
|
|
2
2
|
//# sourceMappingURL=use-install-prompt.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-install-prompt.cjs","names":[],"sources":["../../src/hooks/use-install-prompt.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport {\n buildOpenInChromeIntent,\n isAndroidWithoutPromptApi,\n isIOS,\n isStandalone,\n type BeforeInstallPromptEvent,\n} from \"./pwa-env\";\n\nconst DEFAULT_DECLINE_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MANUAL_FALLBACK_DELAY_MS = 3000;\nconst DEFAULT_DECLINE_STORAGE_KEY = \"tempest:install-declined-at\";\n\nlet cachedEvent: BeforeInstallPromptEvent | null = null;\nconst eventSubscribers = new Set<(event: BeforeInstallPromptEvent | null) => void>();\n\nif (typeof window !== \"undefined\") {\n window.addEventListener(\"beforeinstallprompt\", (event: Event) => {\n event.preventDefault();\n cachedEvent = event as BeforeInstallPromptEvent;\n eventSubscribers.forEach((fn) => fn(cachedEvent));\n });\n window.addEventListener(\"appinstalled\", () => {\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n });\n}\n\n/**\n * Strategy the UI should use to install the app:\n *\n * - `\"native\"`: trigger `BeforeInstallPromptEvent.prompt()`.\n * - `\"ios\"`: show iOS Safari Share → Add to Home Screen instructions.\n * - `\"manual\"`: show generic browser-menu instructions (Chromium forks that\n * strip the prompt API, plus the timeout fallback when no event arrives).\n * - `\"none\"`: nothing to offer (already installed or unsupported runtime).\n */\nexport type InstallMethod = \"native\" | \"ios\" | \"manual\" | \"none\";\n\n/** Options for {@link useInstallPrompt}. */\nexport interface UseInstallPromptOptions {\n /**\n * `localStorage` key used to persist the decline timestamp. Defaults to\n * `\"tempest:install-declined-at\"`.\n */\n declineStorageKey?: string;\n /**\n * How long, in ms, the install CTA stays hidden after the user declines.\n * Defaults to 7 days.\n */\n declineCooldownMs?: number;\n /**\n * How long, in ms, to wait for a `beforeinstallprompt` event before\n * resolving to the `\"manual\"` method. Defaults to `3000`.\n */\n manualFallbackDelayMs?: number;\n}\n\n/** State returned by {@link useInstallPrompt}. */\nexport interface UseInstallPromptResult {\n /** The cached `beforeinstallprompt` event, or `null` when unavailable. */\n deferredPrompt: BeforeInstallPromptEvent | null;\n /** True when the app can be installed through any supported method. */\n canInstall: boolean;\n /** True when running on iOS/iPadOS Safari. */\n isIOS: boolean;\n /** True when already running as an installed PWA. */\n isStandalone: boolean;\n /** True when the browser is an Android Chromium fork with no prompt API. */\n isManualAndroid: boolean;\n /** True when no `beforeinstallprompt` arrived within the timeout window. */\n promptTimedOut: boolean;\n /** The resolved install strategy the UI should follow. */\n method: InstallMethod;\n /** An `intent://` URL to re-open the page in Chrome on Android, else `null`. */\n openInChromeIntent: string | null;\n /**\n * Triggers the native prompt.\n *\n * @returns `true` when the user accepted the install, `false` otherwise.\n */\n install: () => Promise<boolean>;\n /** Records that the user declined the CTA, starting the cooldown window. */\n recordDecline: () => void;\n}\n\nfunction readDecline(key: string, cooldownMs: number): boolean {\n if (typeof window === \"undefined\") return false;\n const raw = window.localStorage.getItem(key);\n if (!raw) return false;\n const ts = Number(raw);\n if (!Number.isFinite(ts)) return false;\n return Date.now() - ts < cooldownMs;\n}\n\n/**\n * React hook that resolves how (and whether) to offer PWA installation.\n *\n * It caches the `beforeinstallprompt` event, detects iOS/iPadOS, detects\n * Android Chromium forks that lack the prompt API, detects standalone display\n * mode, and applies a decline cooldown persisted in `localStorage`. The\n * resulting `method` tells the UI which install affordance to render.\n *\n * The decline persistence is pluggable through\n * {@link UseInstallPromptOptions.declineStorageKey} and\n * {@link UseInstallPromptOptions.declineCooldownMs} — no app-specific storage\n * layer is required, and it is SSR-guarded.\n *\n * @param options - Optional storage key, cooldown, and fallback-delay tuning.\n * @returns The install state plus `install()` and `recordDecline()` actions.\n *\n * @example\n * const { method, install } = useInstallPrompt();\n * if (method === \"native\") return <button onClick={install}>Install</button>;\n */\nexport function useInstallPrompt(options: UseInstallPromptOptions = {}): UseInstallPromptResult {\n const {\n declineStorageKey = DEFAULT_DECLINE_STORAGE_KEY,\n declineCooldownMs = DEFAULT_DECLINE_COOLDOWN_MS,\n manualFallbackDelayMs = DEFAULT_MANUAL_FALLBACK_DELAY_MS,\n } = options;\n\n const [event, setEvent] = useState<BeforeInstallPromptEvent | null>(cachedEvent);\n const [standalone, setStandalone] = useState<boolean>(isStandalone());\n const [promptTimedOut, setPromptTimedOut] = useState<boolean>(false);\n\n useEffect(() => {\n eventSubscribers.add(setEvent);\n return () => {\n eventSubscribers.delete(setEvent);\n };\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const mq = window.matchMedia?.(\"(display-mode: standalone)\");\n const handler = (): void => setStandalone(isStandalone());\n mq?.addEventListener?.(\"change\", handler);\n window.addEventListener(\"appinstalled\", handler);\n return () => {\n mq?.removeEventListener?.(\"change\", handler);\n window.removeEventListener(\"appinstalled\", handler);\n };\n }, []);\n\n useEffect(() => {\n if (event) {\n setPromptTimedOut(false);\n return;\n }\n const timer = window.setTimeout(() => {\n if (!cachedEvent) setPromptTimedOut(true);\n }, manualFallbackDelayMs);\n return () => window.clearTimeout(timer);\n }, [event, manualFallbackDelayMs]);\n\n const ios = isIOS();\n const manualAndroid = isAndroidWithoutPromptApi();\n\n const declineActive = readDecline(declineStorageKey, declineCooldownMs);\n\n let method: InstallMethod = \"none\";\n if (!standalone && !declineActive) {\n if (event) method = \"native\";\n else if (ios) method = \"ios\";\n else if (manualAndroid || promptTimedOut) method = \"manual\";\n }\n\n const recordDecline = (): void => {\n if (typeof window === \"undefined\") return;\n window.localStorage.setItem(declineStorageKey, String(Date.now()));\n };\n\n const install = async (): Promise<boolean> => {\n if (!event) return false;\n await event.prompt();\n const choice = await event.userChoice;\n if (choice.outcome === \"dismissed\") recordDecline();\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n return choice.outcome === \"accepted\";\n };\n\n return {\n deferredPrompt: event,\n canInstall: method !== \"none\",\n isIOS: ios,\n isStandalone: standalone,\n isManualAndroid: manualAndroid,\n promptTimedOut,\n method,\n openInChromeIntent: buildOpenInChromeIntent(),\n install,\n recordDecline,\n };\n}\n"],"mappings":"wDASA,IAAM,EAA8B,OAC9B,EAAmC,IACnC,EAA8B,8BAEhC,EAA+C,KAC7C,EAAmB,IAAI,IAEzB,OAAO,OAAW,MAClB,OAAO,iBAAiB,sBAAwB,GAAiB,CAC7D,EAAM,eAAe,EACrB,EAAc,EACd,EAAiB,QAAS,GAAO,EAAG,CAAW,CAAC,CACpD,CAAC,EACD,OAAO,iBAAiB,mBAAsB,CAC1C,EAAc,KACd,EAAiB,QAAS,GAAO,EAAG,IAAI,CAAC,CAC7C,CAAC,GA6DL,SAAS,EAAY,EAAa,EAA6B,CAC3D,GAAI,OAAO,OAAW,IAAa,MAAO,GAC1C,IAAM,EAAM,OAAO,aAAa,QAAQ,CAAG,EAC3C,GAAI,CAAC,EAAK,MAAO,GACjB,IAAM,EAAK,OAAO,CAAG,EAErB,OADK,OAAO,SAAS,CAAE,EAChB,KAAK,IAAI,EAAI,EAAK,EADQ,EAErC,CAsBA,SAAgB,EAAiB,EAAmC,CAAC,EAA2B,CAC5F,GAAM,CACF,oBAAoB,EACpB,oBAAoB,EACpB,wBAAwB,GACxB,EAEE,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAA0C,CAAW,EACzE,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,CAAkB,EAAA,aAAa,CAAC,EAC9D,CAAC,EAAgB,IAAA,EAAqB,EAAA,SAAA,CAAkB,EAAK,GAEnE,EAAA,EAAA,UAAA,MACI,EAAiB,IAAI,CAAQ,MAChB,CACT,EAAiB,OAAO,CAAQ,CACpC,GACD,CAAC,CAAC,GAEL,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,OAAO,OAAW,IAAa,OACnC,IAAM,EAAK,OAAO,aAAa,4BAA4B,EACrD,MAAsB,EAAc,EAAA,aAAa,CAAC,EAGxD,OAFA,GAAI,mBAAmB,SAAU,CAAO,EACxC,OAAO,iBAAiB,eAAgB,CAAO,MAClC,CACT,GAAI,sBAAsB,SAAU,CAAO,EAC3C,OAAO,oBAAoB,eAAgB,CAAO,CACtD,CACJ,EAAG,CAAC,CAAC,GAEL,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,EAAO,CACP,EAAkB,EAAK,EACvB,MACJ,CACA,IAAM,EAAQ,OAAO,eAAiB,CAC7B,GAAa,EAAkB,EAAI,CAC5C,EAAG,CAAqB,EACxB,UAAa,OAAO,aAAa,CAAK,CAC1C,EAAG,CAAC,EAAO,CAAqB,CAAC,EAEjC,IAAM,EAAM,EAAA,MAAM,EACZ,EAAgB,EAAA,0BAA0B,EAE1C,EAAgB,EAAY,EAAmB,CAAiB,EAElE,EAAwB,OACxB,CAAC,GAAc,CAAC,IACZ,EAAO,EAAS,SACX,EAAK,EAAS,OACd,GAAiB,KAAgB,EAAS,WAGvD,IAAM,MAA4B,CAC1B,OAAO,OAAW,KACtB,OAAO,aAAa,QAAQ,EAAmB,OAAO,KAAK,IAAI,CAAC,CAAC,CACrE,EAYA,MAAO,CACH,eAAgB,EAChB,WAAY,IAAW,OACvB,MAAO,EACP,aAAc,EACd,gBAAiB,EACjB,iBACA,SACA,mBAAoB,EAAA,wBAAwB,EAC5C,iBAnB0C,CAC1C,GAAI,CAAC,EAAO,MAAO,GACnB,MAAM,EAAM,OAAO,EACnB,IAAM,EAAS,MAAM,EAAM,WAI3B,OAHI,EAAO,UAAY,aAAa,EAAc,EAClD,EAAc,KACd,EAAiB,QAAS,GAAO,EAAG,IAAI,CAAC,EAClC,EAAO,UAAY,UAC9B,EAYI,eACJ,CACJ"}
|
|
1
|
+
{"version":3,"file":"use-install-prompt.cjs","names":[],"sources":["../../src/hooks/use-install-prompt.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport {\n buildOpenInChromeIntent,\n isAndroidWithoutPromptApi,\n isIOS,\n isStandalone,\n type BeforeInstallPromptEvent,\n} from \"./pwa-env\";\n\nconst DEFAULT_DECLINE_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MANUAL_FALLBACK_DELAY_MS = 3000;\nconst DEFAULT_DECLINE_STORAGE_KEY = \"tempest:install-declined-at\";\n\nlet cachedEvent: BeforeInstallPromptEvent | null = null;\nconst eventSubscribers = new Set<(event: BeforeInstallPromptEvent | null) => void>();\n\nif (typeof window !== \"undefined\") {\n window.addEventListener(\"beforeinstallprompt\", (event: Event) => {\n event.preventDefault();\n cachedEvent = event as BeforeInstallPromptEvent;\n eventSubscribers.forEach((fn) => fn(cachedEvent));\n });\n window.addEventListener(\"appinstalled\", () => {\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n });\n}\n\n/**\n * Strategy the UI should use to install the app:\n *\n * - `\"native\"`: trigger `BeforeInstallPromptEvent.prompt()`.\n * - `\"ios\"`: show iOS Safari Share → Add to Home Screen instructions.\n * - `\"manual\"`: show generic browser-menu instructions (Chromium forks that\n * strip the prompt API, plus the timeout fallback when no event arrives).\n * - `\"none\"`: nothing to offer (already installed or unsupported runtime).\n */\nexport type InstallMethod = \"native\" | \"ios\" | \"manual\" | \"none\";\n\n/** Options for {@link useInstallPrompt}. */\nexport interface UseInstallPromptOptions {\n /**\n * `localStorage` key used to persist the decline timestamp. Defaults to\n * `\"tempest:install-declined-at\"`.\n */\n declineStorageKey?: string;\n /**\n * How long, in ms, the install CTA stays hidden after the user declines.\n * Defaults to 7 days.\n */\n declineCooldownMs?: number;\n /**\n * How long, in ms, to wait for a `beforeinstallprompt` event before\n * resolving to the `\"manual\"` method. Defaults to `3000`.\n */\n manualFallbackDelayMs?: number;\n}\n\n/** State returned by {@link useInstallPrompt}. */\nexport interface UseInstallPromptResult {\n /** The cached `beforeinstallprompt` event, or `null` when unavailable. */\n deferredPrompt: BeforeInstallPromptEvent | null;\n /** True when the app can be installed through any supported method. */\n canInstall: boolean;\n /** True when running on iOS/iPadOS Safari. */\n isIOS: boolean;\n /** True when already running as an installed PWA. */\n isStandalone: boolean;\n /** True when the browser is an Android Chromium fork with no prompt API. */\n isManualAndroid: boolean;\n /** True when no `beforeinstallprompt` arrived within the timeout window. */\n promptTimedOut: boolean;\n /** The resolved install strategy the UI should follow. */\n method: InstallMethod;\n /** An `intent://` URL to re-open the page in Chrome on Android, else `null`. */\n openInChromeIntent: string | null;\n /**\n * Triggers the native prompt.\n *\n * @returns `true` when the user accepted the install, `false` otherwise.\n */\n install: () => Promise<boolean>;\n /** Records that the user declined the CTA, starting the cooldown window. */\n recordDecline: () => void;\n}\n\n/**\n * Whether a recorded decline is still inside its cooldown window.\n *\n * Reading is guarded because `localStorage.getItem` **throws** where this hook\n * matters most: Safari private mode and a cross-origin frame with storage\n * blocked. Unguarded, that exception escapes during render and takes the page\n * down — the install CTA is a courtesy, and no courtesy is worth a blank screen.\n *\n * @param key - Storage key holding the decline timestamp.\n * @param cooldownMs - How long a decline suppresses the CTA.\n * @returns Whether the CTA should stay hidden.\n */\nfunction readDecline(key: string, cooldownMs: number): boolean {\n if (typeof window === \"undefined\") return false;\n let raw: string | null;\n try {\n raw = window.localStorage.getItem(key);\n } catch {\n return false;\n }\n if (!raw) return false;\n const ts = Number(raw);\n if (!Number.isFinite(ts)) return false;\n return Date.now() - ts < cooldownMs;\n}\n\n/**\n * React hook that resolves how (and whether) to offer PWA installation.\n *\n * It caches the `beforeinstallprompt` event, detects iOS/iPadOS, detects\n * Android Chromium forks that lack the prompt API, detects standalone display\n * mode, and applies a decline cooldown persisted in `localStorage`. The\n * resulting `method` tells the UI which install affordance to render.\n *\n * Both sides of that persistence are best-effort: `localStorage` throws in\n * Safari private mode and in a cross-origin frame with storage blocked, and an\n * exception raised while resolving an install CTA would take the page down with\n * it. A refused read means \"no decline recorded\"; a refused write means the CTA\n * may come back next visit.\n *\n * The decline persistence is pluggable through\n * {@link UseInstallPromptOptions.declineStorageKey} and\n * {@link UseInstallPromptOptions.declineCooldownMs} — no app-specific storage\n * layer is required, and it is SSR-guarded.\n *\n * @param options - Optional storage key, cooldown, and fallback-delay tuning.\n * @returns The install state plus `install()` and `recordDecline()` actions.\n *\n * @example\n * const { method, install } = useInstallPrompt();\n * if (method === \"native\") return <button onClick={install}>Install</button>;\n */\nexport function useInstallPrompt(options: UseInstallPromptOptions = {}): UseInstallPromptResult {\n const {\n declineStorageKey = DEFAULT_DECLINE_STORAGE_KEY,\n declineCooldownMs = DEFAULT_DECLINE_COOLDOWN_MS,\n manualFallbackDelayMs = DEFAULT_MANUAL_FALLBACK_DELAY_MS,\n } = options;\n\n const [event, setEvent] = useState<BeforeInstallPromptEvent | null>(cachedEvent);\n const [standalone, setStandalone] = useState<boolean>(isStandalone());\n const [promptTimedOut, setPromptTimedOut] = useState<boolean>(false);\n\n useEffect(() => {\n eventSubscribers.add(setEvent);\n return () => {\n eventSubscribers.delete(setEvent);\n };\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const mq = window.matchMedia?.(\"(display-mode: standalone)\");\n const handler = (): void => setStandalone(isStandalone());\n mq?.addEventListener?.(\"change\", handler);\n window.addEventListener(\"appinstalled\", handler);\n return () => {\n mq?.removeEventListener?.(\"change\", handler);\n window.removeEventListener(\"appinstalled\", handler);\n };\n }, []);\n\n useEffect(() => {\n if (event) {\n setPromptTimedOut(false);\n return;\n }\n const timer = window.setTimeout(() => {\n if (!cachedEvent) setPromptTimedOut(true);\n }, manualFallbackDelayMs);\n return () => window.clearTimeout(timer);\n }, [event, manualFallbackDelayMs]);\n\n const ios = isIOS();\n const manualAndroid = isAndroidWithoutPromptApi();\n\n const declineActive = readDecline(declineStorageKey, declineCooldownMs);\n\n let method: InstallMethod = \"none\";\n if (!standalone && !declineActive) {\n if (event) method = \"native\";\n else if (ios) method = \"ios\";\n else if (manualAndroid || promptTimedOut) method = \"manual\";\n }\n\n const recordDecline = (): void => {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(declineStorageKey, String(Date.now()));\n } catch {\n /* empty */\n }\n };\n\n const install = async (): Promise<boolean> => {\n if (!event) return false;\n await event.prompt();\n const choice = await event.userChoice;\n if (choice.outcome === \"dismissed\") recordDecline();\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n return choice.outcome === \"accepted\";\n };\n\n return {\n deferredPrompt: event,\n canInstall: method !== \"none\",\n isIOS: ios,\n isStandalone: standalone,\n isManualAndroid: manualAndroid,\n promptTimedOut,\n method,\n openInChromeIntent: buildOpenInChromeIntent(),\n install,\n recordDecline,\n };\n}\n"],"mappings":"wDASA,IAAM,EAA8B,OAC9B,EAAmC,IACnC,EAA8B,8BAEhC,EAA+C,KAC7C,EAAmB,IAAI,IAEzB,OAAO,OAAW,MAClB,OAAO,iBAAiB,sBAAwB,GAAiB,CAC7D,EAAM,eAAe,EACrB,EAAc,EACd,EAAiB,QAAS,GAAO,EAAG,CAAW,CAAC,CACpD,CAAC,EACD,OAAO,iBAAiB,mBAAsB,CAC1C,EAAc,KACd,EAAiB,QAAS,GAAO,EAAG,IAAI,CAAC,CAC7C,CAAC,GAyEL,SAAS,EAAY,EAAa,EAA6B,CAC3D,GAAI,OAAO,OAAW,IAAa,MAAO,GAC1C,IAAI,EACJ,GAAI,CACA,EAAM,OAAO,aAAa,QAAQ,CAAG,CACzC,MAAQ,CACJ,MAAO,EACX,CACA,GAAI,CAAC,EAAK,MAAO,GACjB,IAAM,EAAK,OAAO,CAAG,EAErB,OADK,OAAO,SAAS,CAAE,EAChB,KAAK,IAAI,EAAI,EAAK,EADQ,EAErC,CA4BA,SAAgB,EAAiB,EAAmC,CAAC,EAA2B,CAC5F,GAAM,CACF,oBAAoB,EACpB,oBAAoB,EACpB,wBAAwB,GACxB,EAEE,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAA0C,CAAW,EACzE,CAAC,EAAY,IAAA,EAAiB,EAAA,SAAA,CAAkB,EAAA,aAAa,CAAC,EAC9D,CAAC,EAAgB,IAAA,EAAqB,EAAA,SAAA,CAAkB,EAAK,GAEnE,EAAA,EAAA,UAAA,MACI,EAAiB,IAAI,CAAQ,MAChB,CACT,EAAiB,OAAO,CAAQ,CACpC,GACD,CAAC,CAAC,GAEL,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,OAAO,OAAW,IAAa,OACnC,IAAM,EAAK,OAAO,aAAa,4BAA4B,EACrD,MAAsB,EAAc,EAAA,aAAa,CAAC,EAGxD,OAFA,GAAI,mBAAmB,SAAU,CAAO,EACxC,OAAO,iBAAiB,eAAgB,CAAO,MAClC,CACT,GAAI,sBAAsB,SAAU,CAAO,EAC3C,OAAO,oBAAoB,eAAgB,CAAO,CACtD,CACJ,EAAG,CAAC,CAAC,GAEL,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,EAAO,CACP,EAAkB,EAAK,EACvB,MACJ,CACA,IAAM,EAAQ,OAAO,eAAiB,CAC7B,GAAa,EAAkB,EAAI,CAC5C,EAAG,CAAqB,EACxB,UAAa,OAAO,aAAa,CAAK,CAC1C,EAAG,CAAC,EAAO,CAAqB,CAAC,EAEjC,IAAM,EAAM,EAAA,MAAM,EACZ,EAAgB,EAAA,0BAA0B,EAE1C,EAAgB,EAAY,EAAmB,CAAiB,EAElE,EAAwB,OACxB,CAAC,GAAc,CAAC,IACZ,EAAO,EAAS,SACX,EAAK,EAAS,OACd,GAAiB,KAAgB,EAAS,WAGvD,IAAM,MAA4B,CAC1B,YAAO,OAAW,KACtB,GAAI,CACA,OAAO,aAAa,QAAQ,EAAmB,OAAO,KAAK,IAAI,CAAC,CAAC,CACrE,MAAQ,CAER,CACJ,EAYA,MAAO,CACH,eAAgB,EAChB,WAAY,IAAW,OACvB,MAAO,EACP,aAAc,EACd,gBAAiB,EACjB,iBACA,SACA,mBAAoB,EAAA,wBAAwB,EAC5C,iBAnB0C,CAC1C,GAAI,CAAC,EAAO,MAAO,GACnB,MAAM,EAAM,OAAO,EACnB,IAAM,EAAS,MAAM,EAAM,WAI3B,OAHI,EAAO,UAAY,aAAa,EAAc,EAClD,EAAc,KACd,EAAiB,QAAS,GAAO,EAAG,IAAI,CAAC,EAClC,EAAO,UAAY,UAC9B,EAYI,eACJ,CACJ"}
|
|
@@ -9,7 +9,12 @@ typeof window < "u" && (window.addEventListener("beforeinstallprompt", (e) => {
|
|
|
9
9
|
}));
|
|
10
10
|
function d(e, t) {
|
|
11
11
|
if (typeof window > "u") return !1;
|
|
12
|
-
let n
|
|
12
|
+
let n;
|
|
13
|
+
try {
|
|
14
|
+
n = window.localStorage.getItem(e);
|
|
15
|
+
} catch {
|
|
16
|
+
return !1;
|
|
17
|
+
}
|
|
13
18
|
if (!n) return !1;
|
|
14
19
|
let r = Number(n);
|
|
15
20
|
return Number.isFinite(r) ? Date.now() - r < t : !1;
|
|
@@ -37,7 +42,9 @@ function f(f = {}) {
|
|
|
37
42
|
let S = n(), C = t(), w = d(p, m), T = "none";
|
|
38
43
|
!v && !w && (g ? T = "native" : S ? T = "ios" : (C || b) && (T = "manual"));
|
|
39
44
|
let E = () => {
|
|
40
|
-
typeof window > "u"
|
|
45
|
+
if (!(typeof window > "u")) try {
|
|
46
|
+
window.localStorage.setItem(p, String(Date.now()));
|
|
47
|
+
} catch {}
|
|
41
48
|
};
|
|
42
49
|
return {
|
|
43
50
|
deferredPrompt: g,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-install-prompt.js","names":[],"sources":["../../src/hooks/use-install-prompt.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport {\n buildOpenInChromeIntent,\n isAndroidWithoutPromptApi,\n isIOS,\n isStandalone,\n type BeforeInstallPromptEvent,\n} from \"./pwa-env\";\n\nconst DEFAULT_DECLINE_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MANUAL_FALLBACK_DELAY_MS = 3000;\nconst DEFAULT_DECLINE_STORAGE_KEY = \"tempest:install-declined-at\";\n\nlet cachedEvent: BeforeInstallPromptEvent | null = null;\nconst eventSubscribers = new Set<(event: BeforeInstallPromptEvent | null) => void>();\n\nif (typeof window !== \"undefined\") {\n window.addEventListener(\"beforeinstallprompt\", (event: Event) => {\n event.preventDefault();\n cachedEvent = event as BeforeInstallPromptEvent;\n eventSubscribers.forEach((fn) => fn(cachedEvent));\n });\n window.addEventListener(\"appinstalled\", () => {\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n });\n}\n\n/**\n * Strategy the UI should use to install the app:\n *\n * - `\"native\"`: trigger `BeforeInstallPromptEvent.prompt()`.\n * - `\"ios\"`: show iOS Safari Share → Add to Home Screen instructions.\n * - `\"manual\"`: show generic browser-menu instructions (Chromium forks that\n * strip the prompt API, plus the timeout fallback when no event arrives).\n * - `\"none\"`: nothing to offer (already installed or unsupported runtime).\n */\nexport type InstallMethod = \"native\" | \"ios\" | \"manual\" | \"none\";\n\n/** Options for {@link useInstallPrompt}. */\nexport interface UseInstallPromptOptions {\n /**\n * `localStorage` key used to persist the decline timestamp. Defaults to\n * `\"tempest:install-declined-at\"`.\n */\n declineStorageKey?: string;\n /**\n * How long, in ms, the install CTA stays hidden after the user declines.\n * Defaults to 7 days.\n */\n declineCooldownMs?: number;\n /**\n * How long, in ms, to wait for a `beforeinstallprompt` event before\n * resolving to the `\"manual\"` method. Defaults to `3000`.\n */\n manualFallbackDelayMs?: number;\n}\n\n/** State returned by {@link useInstallPrompt}. */\nexport interface UseInstallPromptResult {\n /** The cached `beforeinstallprompt` event, or `null` when unavailable. */\n deferredPrompt: BeforeInstallPromptEvent | null;\n /** True when the app can be installed through any supported method. */\n canInstall: boolean;\n /** True when running on iOS/iPadOS Safari. */\n isIOS: boolean;\n /** True when already running as an installed PWA. */\n isStandalone: boolean;\n /** True when the browser is an Android Chromium fork with no prompt API. */\n isManualAndroid: boolean;\n /** True when no `beforeinstallprompt` arrived within the timeout window. */\n promptTimedOut: boolean;\n /** The resolved install strategy the UI should follow. */\n method: InstallMethod;\n /** An `intent://` URL to re-open the page in Chrome on Android, else `null`. */\n openInChromeIntent: string | null;\n /**\n * Triggers the native prompt.\n *\n * @returns `true` when the user accepted the install, `false` otherwise.\n */\n install: () => Promise<boolean>;\n /** Records that the user declined the CTA, starting the cooldown window. */\n recordDecline: () => void;\n}\n\nfunction readDecline(key: string, cooldownMs: number): boolean {\n if (typeof window === \"undefined\") return false;\n const raw = window.localStorage.getItem(key);\n if (!raw) return false;\n const ts = Number(raw);\n if (!Number.isFinite(ts)) return false;\n return Date.now() - ts < cooldownMs;\n}\n\n/**\n * React hook that resolves how (and whether) to offer PWA installation.\n *\n * It caches the `beforeinstallprompt` event, detects iOS/iPadOS, detects\n * Android Chromium forks that lack the prompt API, detects standalone display\n * mode, and applies a decline cooldown persisted in `localStorage`. The\n * resulting `method` tells the UI which install affordance to render.\n *\n * The decline persistence is pluggable through\n * {@link UseInstallPromptOptions.declineStorageKey} and\n * {@link UseInstallPromptOptions.declineCooldownMs} — no app-specific storage\n * layer is required, and it is SSR-guarded.\n *\n * @param options - Optional storage key, cooldown, and fallback-delay tuning.\n * @returns The install state plus `install()` and `recordDecline()` actions.\n *\n * @example\n * const { method, install } = useInstallPrompt();\n * if (method === \"native\") return <button onClick={install}>Install</button>;\n */\nexport function useInstallPrompt(options: UseInstallPromptOptions = {}): UseInstallPromptResult {\n const {\n declineStorageKey = DEFAULT_DECLINE_STORAGE_KEY,\n declineCooldownMs = DEFAULT_DECLINE_COOLDOWN_MS,\n manualFallbackDelayMs = DEFAULT_MANUAL_FALLBACK_DELAY_MS,\n } = options;\n\n const [event, setEvent] = useState<BeforeInstallPromptEvent | null>(cachedEvent);\n const [standalone, setStandalone] = useState<boolean>(isStandalone());\n const [promptTimedOut, setPromptTimedOut] = useState<boolean>(false);\n\n useEffect(() => {\n eventSubscribers.add(setEvent);\n return () => {\n eventSubscribers.delete(setEvent);\n };\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const mq = window.matchMedia?.(\"(display-mode: standalone)\");\n const handler = (): void => setStandalone(isStandalone());\n mq?.addEventListener?.(\"change\", handler);\n window.addEventListener(\"appinstalled\", handler);\n return () => {\n mq?.removeEventListener?.(\"change\", handler);\n window.removeEventListener(\"appinstalled\", handler);\n };\n }, []);\n\n useEffect(() => {\n if (event) {\n setPromptTimedOut(false);\n return;\n }\n const timer = window.setTimeout(() => {\n if (!cachedEvent) setPromptTimedOut(true);\n }, manualFallbackDelayMs);\n return () => window.clearTimeout(timer);\n }, [event, manualFallbackDelayMs]);\n\n const ios = isIOS();\n const manualAndroid = isAndroidWithoutPromptApi();\n\n const declineActive = readDecline(declineStorageKey, declineCooldownMs);\n\n let method: InstallMethod = \"none\";\n if (!standalone && !declineActive) {\n if (event) method = \"native\";\n else if (ios) method = \"ios\";\n else if (manualAndroid || promptTimedOut) method = \"manual\";\n }\n\n const recordDecline = (): void => {\n if (typeof window === \"undefined\") return;\n window.localStorage.setItem(declineStorageKey, String(Date.now()));\n };\n\n const install = async (): Promise<boolean> => {\n if (!event) return false;\n await event.prompt();\n const choice = await event.userChoice;\n if (choice.outcome === \"dismissed\") recordDecline();\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n return choice.outcome === \"accepted\";\n };\n\n return {\n deferredPrompt: event,\n canInstall: method !== \"none\",\n isIOS: ios,\n isStandalone: standalone,\n isManualAndroid: manualAndroid,\n promptTimedOut,\n method,\n openInChromeIntent: buildOpenInChromeIntent(),\n install,\n recordDecline,\n };\n}\n"],"mappings":";;;AASA,IAAM,IAA8B,QAC9B,IAAmC,KACnC,IAA8B,+BAEhC,IAA+C,MAC7C,oBAAmB,IAAI,IAAsD;AAE/E,OAAO,SAAW,QAClB,OAAO,iBAAiB,wBAAwB,MAAiB;CAG7D,AAFA,EAAM,eAAe,GACrB,IAAc,GACd,EAAiB,SAAS,MAAO,EAAG,CAAW,CAAC;AACpD,CAAC,GACD,OAAO,iBAAiB,sBAAsB;CAE1C,AADA,IAAc,MACd,EAAiB,SAAS,MAAO,EAAG,IAAI,CAAC;AAC7C,CAAC;AA6DL,SAAS,EAAY,GAAa,GAA6B;CAC3D,IAAI,OAAO,SAAW,KAAa,OAAO;CAC1C,IAAM,IAAM,OAAO,aAAa,QAAQ,CAAG;CAC3C,IAAI,CAAC,GAAK,OAAO;CACjB,IAAM,IAAK,OAAO,CAAG;CAErB,OADK,OAAO,SAAS,CAAE,IAChB,KAAK,IAAI,IAAI,IAAK,IADQ;AAErC;AAsBA,SAAgB,EAAiB,IAAmC,CAAC,GAA2B;CAC5F,IAAM,EACF,uBAAoB,GACpB,uBAAoB,GACpB,2BAAwB,MACxB,GAEE,CAAC,GAAO,KAAY,EAA0C,CAAW,GACzE,CAAC,GAAY,KAAiB,EAAkB,EAAa,CAAC,GAC9D,CAAC,GAAgB,KAAqB,EAAkB,EAAK;CAqBnE,AAnBA,SACI,EAAiB,IAAI,CAAQ,SAChB;EACT,EAAiB,OAAO,CAAQ;CACpC,IACD,CAAC,CAAC,GAEL,QAAgB;EACZ,IAAI,OAAO,SAAW,KAAa;EACnC,IAAM,IAAK,OAAO,aAAa,4BAA4B,GACrD,UAAsB,EAAc,EAAa,CAAC;EAGxD,OAFA,GAAI,mBAAmB,UAAU,CAAO,GACxC,OAAO,iBAAiB,gBAAgB,CAAO,SAClC;GAET,AADA,GAAI,sBAAsB,UAAU,CAAO,GAC3C,OAAO,oBAAoB,gBAAgB,CAAO;EACtD;CACJ,GAAG,CAAC,CAAC,GAEL,QAAgB;EACZ,IAAI,GAAO;GACP,EAAkB,EAAK;GACvB;EACJ;EACA,IAAM,IAAQ,OAAO,iBAAiB;GAClC,AAAK,KAAa,EAAkB,EAAI;EAC5C,GAAG,CAAqB;EACxB,aAAa,OAAO,aAAa,CAAK;CAC1C,GAAG,CAAC,GAAO,CAAqB,CAAC;CAEjC,IAAM,IAAM,EAAM,GACZ,IAAgB,EAA0B,GAE1C,IAAgB,EAAY,GAAmB,CAAiB,GAElE,IAAwB;CAC5B,AAAI,CAAC,KAAc,CAAC,MACZ,IAAO,IAAS,WACX,IAAK,IAAS,SACd,KAAiB,OAAgB,IAAS;CAGvD,IAAM,UAA4B;EAC1B,OAAO,SAAW,OACtB,OAAO,aAAa,QAAQ,GAAmB,OAAO,KAAK,IAAI,CAAC,CAAC;CACrE;CAYA,OAAO;EACH,gBAAgB;EAChB,YAAY,MAAW;EACvB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB;EACA;EACA,oBAAoB,EAAwB;EAC5C,qBAnB0C;GAC1C,IAAI,CAAC,GAAO,OAAO;GACnB,MAAM,EAAM,OAAO;GACnB,IAAM,IAAS,MAAM,EAAM;GAI3B,OAHI,EAAO,YAAY,eAAa,EAAc,GAClD,IAAc,MACd,EAAiB,SAAS,MAAO,EAAG,IAAI,CAAC,GAClC,EAAO,YAAY;EAC9B;EAYI;CACJ;AACJ"}
|
|
1
|
+
{"version":3,"file":"use-install-prompt.js","names":[],"sources":["../../src/hooks/use-install-prompt.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport {\n buildOpenInChromeIntent,\n isAndroidWithoutPromptApi,\n isIOS,\n isStandalone,\n type BeforeInstallPromptEvent,\n} from \"./pwa-env\";\n\nconst DEFAULT_DECLINE_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_MANUAL_FALLBACK_DELAY_MS = 3000;\nconst DEFAULT_DECLINE_STORAGE_KEY = \"tempest:install-declined-at\";\n\nlet cachedEvent: BeforeInstallPromptEvent | null = null;\nconst eventSubscribers = new Set<(event: BeforeInstallPromptEvent | null) => void>();\n\nif (typeof window !== \"undefined\") {\n window.addEventListener(\"beforeinstallprompt\", (event: Event) => {\n event.preventDefault();\n cachedEvent = event as BeforeInstallPromptEvent;\n eventSubscribers.forEach((fn) => fn(cachedEvent));\n });\n window.addEventListener(\"appinstalled\", () => {\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n });\n}\n\n/**\n * Strategy the UI should use to install the app:\n *\n * - `\"native\"`: trigger `BeforeInstallPromptEvent.prompt()`.\n * - `\"ios\"`: show iOS Safari Share → Add to Home Screen instructions.\n * - `\"manual\"`: show generic browser-menu instructions (Chromium forks that\n * strip the prompt API, plus the timeout fallback when no event arrives).\n * - `\"none\"`: nothing to offer (already installed or unsupported runtime).\n */\nexport type InstallMethod = \"native\" | \"ios\" | \"manual\" | \"none\";\n\n/** Options for {@link useInstallPrompt}. */\nexport interface UseInstallPromptOptions {\n /**\n * `localStorage` key used to persist the decline timestamp. Defaults to\n * `\"tempest:install-declined-at\"`.\n */\n declineStorageKey?: string;\n /**\n * How long, in ms, the install CTA stays hidden after the user declines.\n * Defaults to 7 days.\n */\n declineCooldownMs?: number;\n /**\n * How long, in ms, to wait for a `beforeinstallprompt` event before\n * resolving to the `\"manual\"` method. Defaults to `3000`.\n */\n manualFallbackDelayMs?: number;\n}\n\n/** State returned by {@link useInstallPrompt}. */\nexport interface UseInstallPromptResult {\n /** The cached `beforeinstallprompt` event, or `null` when unavailable. */\n deferredPrompt: BeforeInstallPromptEvent | null;\n /** True when the app can be installed through any supported method. */\n canInstall: boolean;\n /** True when running on iOS/iPadOS Safari. */\n isIOS: boolean;\n /** True when already running as an installed PWA. */\n isStandalone: boolean;\n /** True when the browser is an Android Chromium fork with no prompt API. */\n isManualAndroid: boolean;\n /** True when no `beforeinstallprompt` arrived within the timeout window. */\n promptTimedOut: boolean;\n /** The resolved install strategy the UI should follow. */\n method: InstallMethod;\n /** An `intent://` URL to re-open the page in Chrome on Android, else `null`. */\n openInChromeIntent: string | null;\n /**\n * Triggers the native prompt.\n *\n * @returns `true` when the user accepted the install, `false` otherwise.\n */\n install: () => Promise<boolean>;\n /** Records that the user declined the CTA, starting the cooldown window. */\n recordDecline: () => void;\n}\n\n/**\n * Whether a recorded decline is still inside its cooldown window.\n *\n * Reading is guarded because `localStorage.getItem` **throws** where this hook\n * matters most: Safari private mode and a cross-origin frame with storage\n * blocked. Unguarded, that exception escapes during render and takes the page\n * down — the install CTA is a courtesy, and no courtesy is worth a blank screen.\n *\n * @param key - Storage key holding the decline timestamp.\n * @param cooldownMs - How long a decline suppresses the CTA.\n * @returns Whether the CTA should stay hidden.\n */\nfunction readDecline(key: string, cooldownMs: number): boolean {\n if (typeof window === \"undefined\") return false;\n let raw: string | null;\n try {\n raw = window.localStorage.getItem(key);\n } catch {\n return false;\n }\n if (!raw) return false;\n const ts = Number(raw);\n if (!Number.isFinite(ts)) return false;\n return Date.now() - ts < cooldownMs;\n}\n\n/**\n * React hook that resolves how (and whether) to offer PWA installation.\n *\n * It caches the `beforeinstallprompt` event, detects iOS/iPadOS, detects\n * Android Chromium forks that lack the prompt API, detects standalone display\n * mode, and applies a decline cooldown persisted in `localStorage`. The\n * resulting `method` tells the UI which install affordance to render.\n *\n * Both sides of that persistence are best-effort: `localStorage` throws in\n * Safari private mode and in a cross-origin frame with storage blocked, and an\n * exception raised while resolving an install CTA would take the page down with\n * it. A refused read means \"no decline recorded\"; a refused write means the CTA\n * may come back next visit.\n *\n * The decline persistence is pluggable through\n * {@link UseInstallPromptOptions.declineStorageKey} and\n * {@link UseInstallPromptOptions.declineCooldownMs} — no app-specific storage\n * layer is required, and it is SSR-guarded.\n *\n * @param options - Optional storage key, cooldown, and fallback-delay tuning.\n * @returns The install state plus `install()` and `recordDecline()` actions.\n *\n * @example\n * const { method, install } = useInstallPrompt();\n * if (method === \"native\") return <button onClick={install}>Install</button>;\n */\nexport function useInstallPrompt(options: UseInstallPromptOptions = {}): UseInstallPromptResult {\n const {\n declineStorageKey = DEFAULT_DECLINE_STORAGE_KEY,\n declineCooldownMs = DEFAULT_DECLINE_COOLDOWN_MS,\n manualFallbackDelayMs = DEFAULT_MANUAL_FALLBACK_DELAY_MS,\n } = options;\n\n const [event, setEvent] = useState<BeforeInstallPromptEvent | null>(cachedEvent);\n const [standalone, setStandalone] = useState<boolean>(isStandalone());\n const [promptTimedOut, setPromptTimedOut] = useState<boolean>(false);\n\n useEffect(() => {\n eventSubscribers.add(setEvent);\n return () => {\n eventSubscribers.delete(setEvent);\n };\n }, []);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const mq = window.matchMedia?.(\"(display-mode: standalone)\");\n const handler = (): void => setStandalone(isStandalone());\n mq?.addEventListener?.(\"change\", handler);\n window.addEventListener(\"appinstalled\", handler);\n return () => {\n mq?.removeEventListener?.(\"change\", handler);\n window.removeEventListener(\"appinstalled\", handler);\n };\n }, []);\n\n useEffect(() => {\n if (event) {\n setPromptTimedOut(false);\n return;\n }\n const timer = window.setTimeout(() => {\n if (!cachedEvent) setPromptTimedOut(true);\n }, manualFallbackDelayMs);\n return () => window.clearTimeout(timer);\n }, [event, manualFallbackDelayMs]);\n\n const ios = isIOS();\n const manualAndroid = isAndroidWithoutPromptApi();\n\n const declineActive = readDecline(declineStorageKey, declineCooldownMs);\n\n let method: InstallMethod = \"none\";\n if (!standalone && !declineActive) {\n if (event) method = \"native\";\n else if (ios) method = \"ios\";\n else if (manualAndroid || promptTimedOut) method = \"manual\";\n }\n\n const recordDecline = (): void => {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.setItem(declineStorageKey, String(Date.now()));\n } catch {\n /* empty */\n }\n };\n\n const install = async (): Promise<boolean> => {\n if (!event) return false;\n await event.prompt();\n const choice = await event.userChoice;\n if (choice.outcome === \"dismissed\") recordDecline();\n cachedEvent = null;\n eventSubscribers.forEach((fn) => fn(null));\n return choice.outcome === \"accepted\";\n };\n\n return {\n deferredPrompt: event,\n canInstall: method !== \"none\",\n isIOS: ios,\n isStandalone: standalone,\n isManualAndroid: manualAndroid,\n promptTimedOut,\n method,\n openInChromeIntent: buildOpenInChromeIntent(),\n install,\n recordDecline,\n };\n}\n"],"mappings":";;;AASA,IAAM,IAA8B,QAC9B,IAAmC,KACnC,IAA8B,+BAEhC,IAA+C,MAC7C,oBAAmB,IAAI,IAAsD;AAE/E,OAAO,SAAW,QAClB,OAAO,iBAAiB,wBAAwB,MAAiB;CAG7D,AAFA,EAAM,eAAe,GACrB,IAAc,GACd,EAAiB,SAAS,MAAO,EAAG,CAAW,CAAC;AACpD,CAAC,GACD,OAAO,iBAAiB,sBAAsB;CAE1C,AADA,IAAc,MACd,EAAiB,SAAS,MAAO,EAAG,IAAI,CAAC;AAC7C,CAAC;AAyEL,SAAS,EAAY,GAAa,GAA6B;CAC3D,IAAI,OAAO,SAAW,KAAa,OAAO;CAC1C,IAAI;CACJ,IAAI;EACA,IAAM,OAAO,aAAa,QAAQ,CAAG;CACzC,QAAQ;EACJ,OAAO;CACX;CACA,IAAI,CAAC,GAAK,OAAO;CACjB,IAAM,IAAK,OAAO,CAAG;CAErB,OADK,OAAO,SAAS,CAAE,IAChB,KAAK,IAAI,IAAI,IAAK,IADQ;AAErC;AA4BA,SAAgB,EAAiB,IAAmC,CAAC,GAA2B;CAC5F,IAAM,EACF,uBAAoB,GACpB,uBAAoB,GACpB,2BAAwB,MACxB,GAEE,CAAC,GAAO,KAAY,EAA0C,CAAW,GACzE,CAAC,GAAY,KAAiB,EAAkB,EAAa,CAAC,GAC9D,CAAC,GAAgB,KAAqB,EAAkB,EAAK;CAqBnE,AAnBA,SACI,EAAiB,IAAI,CAAQ,SAChB;EACT,EAAiB,OAAO,CAAQ;CACpC,IACD,CAAC,CAAC,GAEL,QAAgB;EACZ,IAAI,OAAO,SAAW,KAAa;EACnC,IAAM,IAAK,OAAO,aAAa,4BAA4B,GACrD,UAAsB,EAAc,EAAa,CAAC;EAGxD,OAFA,GAAI,mBAAmB,UAAU,CAAO,GACxC,OAAO,iBAAiB,gBAAgB,CAAO,SAClC;GAET,AADA,GAAI,sBAAsB,UAAU,CAAO,GAC3C,OAAO,oBAAoB,gBAAgB,CAAO;EACtD;CACJ,GAAG,CAAC,CAAC,GAEL,QAAgB;EACZ,IAAI,GAAO;GACP,EAAkB,EAAK;GACvB;EACJ;EACA,IAAM,IAAQ,OAAO,iBAAiB;GAClC,AAAK,KAAa,EAAkB,EAAI;EAC5C,GAAG,CAAqB;EACxB,aAAa,OAAO,aAAa,CAAK;CAC1C,GAAG,CAAC,GAAO,CAAqB,CAAC;CAEjC,IAAM,IAAM,EAAM,GACZ,IAAgB,EAA0B,GAE1C,IAAgB,EAAY,GAAmB,CAAiB,GAElE,IAAwB;CAC5B,AAAI,CAAC,KAAc,CAAC,MACZ,IAAO,IAAS,WACX,IAAK,IAAS,SACd,KAAiB,OAAgB,IAAS;CAGvD,IAAM,UAA4B;EAC1B,aAAO,SAAW,MACtB,IAAI;GACA,OAAO,aAAa,QAAQ,GAAmB,OAAO,KAAK,IAAI,CAAC,CAAC;EACrE,QAAQ,CAER;CACJ;CAYA,OAAO;EACH,gBAAgB;EAChB,YAAY,MAAW;EACvB,OAAO;EACP,cAAc;EACd,iBAAiB;EACjB;EACA;EACA,oBAAoB,EAAwB;EAC5C,qBAnB0C;GAC1C,IAAI,CAAC,GAAO,OAAO;GACnB,MAAM,EAAM,OAAO;GACnB,IAAM,IAAS,MAAM,EAAM;GAI3B,OAHI,EAAO,YAAY,eAAa,EAAc,GAClD,IAAc,MACd,EAAiB,SAAS,MAAO,EAAG,IAAI,CAAC,GAClC,EAAO,YAAY;EAC9B;EAYI;CACJ;AACJ"}
|
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("./
|
|
1
|
+
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./decode-response.cjs"),r=require("./errors.cjs"),i=require("./retry.cjs"),a=require("./timeout.cjs");var o=new Set([`GET`,`HEAD`,`OPTIONS`]);function s(e,t){return!o.has(t)||!(e instanceof r.TempestApiError)?!1:r.isRetriableStatus(e.status)}function c(e){return e?e===!0?{}:e:null}var l=15e3,u=3e5;function d(e){return typeof FormData<`u`&&e instanceof FormData}function f(e){return Math.round(performance.now()-e)}async function p(e,t){let n;try{n=await e.clone().json()}catch{try{n=await e.text()}catch{n=null}}return new r.TempestApiError(r.buildApiError(e.status,n,e.headers,t))}function m(o){let m=o.fetcher??globalThis.fetch.bind(globalThis);function h(){let e=o.getToken?.();return e?{Authorization:`Bearer ${e}`}:{}}async function g(e,n,i){let{body:s,params:c,headers:f,signal:p,timeout:g,..._}=n,v=d(s),y=v?o.uploadTimeout:o.timeout,b=g===void 0?y===void 0?v?u:l:y:g,x={...v?{}:{"Content-Type":`application/json`},...i?{"X-Request-ID":i}:{},...o.headers,...h(),...f},S=a.withTimeout(p,b),C={..._,signal:S.signal,headers:x,credentials:o.withCredentials?`include`:_.credentials,body:s==null?void 0:v?s:JSON.stringify(s)},w=t.buildApiUrl(o.baseURL,e,{prefix:o.prefix,params:c});try{return await m(w,C)}catch(e){throw S.timedOut()?new r.TempestApiError({status:0,detail:`A requisição excedeu ${b}ms e foi abandonada.`}):e}finally{S.dispose()}}async function _(e,t,n,r){let i=o.logger;if(!i)return g(e,t,n);let a=performance.now();try{let o=await g(e,t,n),s={requestId:n,status:o.status,ms:f(a)},c=`${r} ${e} → ${o.status}`;return o.status>=400?i.warn(c,s):i.debug(c,s),o}catch(t){throw i.warn(`${r} ${e} → no response`,{requestId:n,ms:f(a),error:t}),t}}async function v(e,t){if(o.onUnauthorized){o.logger?.warn(`unauthorized — calling onUnauthorized`,{requestId:t,status:e.status});try{await o.onUnauthorized(e)}catch(n){o.logger?.warn(`onUnauthorized threw — keeping the original response error`,{requestId:t,status:e.status,error:n})}}}async function y(t,n,r){let i=o.requestId?o.requestId():e.randomId(),a=(n.method??`GET`).toUpperCase(),s=await _(t,n,i,a);if(s.status===401){if(o.refresh){try{await o.refresh(),s=await _(t,n,i,a)}catch{throw await v(s,i),await p(s,i)}s.status===401&&await v(s,i)}else await v(s,i)}if(!s.ok)throw await p(s,i);return r(s)}async function b(e,t,n){let r=c(o.retry);if(!r)return y(e,t,n);let a=(t.method??`GET`).toUpperCase();return i.retry(()=>y(e,t,n),{...r,shouldRetry:r.shouldRetry??(e=>s(e,a))})}async function x(e,t={}){return b(e,t,n.decodeByContentType)}async function S(e,t,n=`POST`,r){return x(e,{...r,method:n,body:t})}return{request:x,get:(e,t)=>x(e,{...t,method:`GET`}),post:(e,t)=>x(e,{...t,method:`POST`}),put:(e,t)=>x(e,{...t,method:`PUT`}),patch:(e,t)=>x(e,{...t,method:`PATCH`}),delete:(e,t)=>x(e,{...t,method:`DELETE`}),blob:(e,t)=>b(e,{...t},e=>e.blob()),arrayBuffer:(e,t)=>b(e,{...t},e=>e.arrayBuffer()),upload:S}}exports.createApiClient=m;
|
|
2
2
|
//# sourceMappingURL=api-client.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n if (response.status === 204) {\n return undefined as T;\n }\n\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\")) {\n return (await response.json()) as T;\n }\n return (await response.text()) as unknown as T;\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n upload,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"api-client.cjs","names":[],"sources":["../../src/http/api-client.ts"],"sourcesContent":["/**\n * @tempest-limits function-lines — createApiClient is over the limit and every line\n * is a request-lifecycle concern the client cannot delegate: base URL joining, the\n * auth header, the 401 refresh-and-replay, the opt-in retry wrapper and the response\n * parsing that turns a failure into a typed error.\n */\nimport { randomId } from \"../utils\";\nimport { buildApiUrl } from \"./build-url\";\nimport { decodeByContentType } from \"./decode-response\";\nimport type { ResponseDecoder } from \"./decode-response\";\nimport { buildApiError, TempestApiError, isRetriableStatus } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\nimport { withTimeout } from \"./timeout\";\nimport type { ApiClient, ApiClientConfig, RequestOptions } from \"./types\";\n\n/**\n * Methods the built-in retry policy will replay.\n *\n * `PUT` and `DELETE` are idempotent on paper but stay out: a backend that logs,\n * bills, or fires a webhook per call still sees two, so replaying them is a\n * decision the caller makes through `shouldRetry`, not a default.\n */\nconst IDEMPOTENT_METHODS: ReadonlySet<string> = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Sub-500 statuses worth a second attempt: a network failure (status `0`), a\n * request timeout, a too-early replay, and a rate limit — which usually carries\n * the `Retry-After` the backoff already honours.\n */\n\n/**\n * The built-in retry policy, used when `retry` is `true` or is options carrying\n * no `shouldRetry` of their own.\n *\n * Conservative on purpose. Replaying a write can duplicate it, and replaying a\n * `400` or a `403` cannot fix a bad payload or a permission the caller does not\n * have — it only spends the user's time before showing the same error.\n *\n * @param error - Whatever the attempt threw.\n * @param method - The upper-cased HTTP method of the request.\n * @returns Whether the client should try again.\n */\nfunction isRetriableFailure(error: unknown, method: string): boolean {\n if (!IDEMPOTENT_METHODS.has(method)) return false;\n if (!(error instanceof TempestApiError)) return false;\n return isRetriableStatus(error.status);\n}\n\n/**\n * Normalize the `retry` config into options, or `null` when retrying is off.\n *\n * @param config - The `retry` field as the caller wrote it.\n * @returns Retry options to use, or `null` to run a single attempt.\n */\nfunction resolveRetry(config: boolean | RetryOptions | undefined): RetryOptions | null {\n if (!config) return null;\n return config === true ? {} : config;\n}\n\n/** Default milliseconds before a request is abandoned. */\nconst DEFAULT_TIMEOUT = 15_000;\n\n/** Default milliseconds before a `FormData` request is abandoned. */\nconst DEFAULT_UPLOAD_TIMEOUT = 300_000;\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\n}\n\n/**\n * Milliseconds since a `performance.now()` reading, rounded.\n *\n * `performance.now()` rather than `Date.now()` because this measures a\n * duration: the wall clock can step sideways mid-request (an NTP correction, a\n * VM resuming, the user changing the clock) and turn a 40 ms call into a\n * negative number. The monotonic clock cannot.\n *\n * @param startedAt - The reading taken before the work started.\n * @returns Whole milliseconds elapsed.\n */\nfunction elapsedMs(startedAt: number): number {\n return Math.round(performance.now() - startedAt);\n}\n\nasync function parseError(response: Response, sentRequestId?: string): Promise<TempestApiError> {\n let body: unknown;\n try {\n body = await response.clone().json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = null;\n }\n }\n return new TempestApiError(\n buildApiError(response.status, body, response.headers, sentRequestId),\n );\n}\n\n/**\n * Create a typed HTTP client backed by `fetch`.\n *\n * Handles JSON serialization, query params, bearer auth via `getToken`, uploads\n * via `FormData`, and throws a typed `ApiError` on any non-2xx response.\n *\n * **Expired sessions.** A `401` with `refresh` configured awaits the refresh and\n * replays the request once. `onUnauthorized` fires whenever that path ends\n * unauthorized anyway — the refresh threw, or the replay came back `401` — which\n * is the signal to clear the session. Without `refresh`, the first `401` calls\n * it directly.\n *\n * **Binary bodies** have their own methods — `blob()` and `arrayBuffer()` — which\n * run the same pipeline and skip the parse. `request()` no longer decodes an\n * unknown `Content-Type` as text either: it returns a `Blob` and says so once in\n * a development build, because reading `image/jpeg` as UTF-8 destroyed the bytes\n * on the way in.\n *\n * **Retries** are off unless you set `retry`. See {@link ApiClientConfig.retry}\n * for the built-in policy; it never replays a write.\n *\n * **Logging** is off unless you pass a `logger`. With one, every finished attempt\n * writes a line — `debug` under 400, `warn` from 400 up, plus a `warn` when\n * `onUnauthorized` fires — carrying `requestId`, `status` and elapsed `ms`, and\n * never a body, header or query string. The level and the destination belong to\n * the logger, not to a boolean here.\n *\n * @example\n * const api = createApiClient({\n * baseURL: import.meta.env.VITE_API_URL,\n * getToken: () => useAuthStore.getState().token,\n * refresh,\n * onUnauthorized: () => useAuthStore.getState().logout(),\n * logger: createLogger({ level: import.meta.env.DEV ? \"debug\" : \"warn\" }).child(\"http\"),\n * retry: true,\n * });\n *\n * @param config - Base URL plus the optional auth, retry and fetch hooks.\n * @returns A client with `request`/`get`/`post`/`put`/`patch`/`delete`/`blob`/\n * `arrayBuffer`/`upload`.\n */\nexport function createApiClient(config: ApiClientConfig): ApiClient {\n const fetcher = config.fetcher ?? globalThis.fetch.bind(globalThis);\n\n function authHeaders(): Record<string, string> {\n const token = config.getToken?.();\n return token ? { Authorization: `Bearer ${token}` } : {};\n }\n\n async function rawRequest(\n path: string,\n options: RequestOptions,\n requestId?: string,\n ): Promise<Response> {\n const { body, params, headers, signal, timeout, ...rest } = options;\n const isForm = isFormData(body);\n const configured = isForm ? config.uploadTimeout : config.timeout;\n const fallback = isForm ? DEFAULT_UPLOAD_TIMEOUT : DEFAULT_TIMEOUT;\n const limit =\n timeout !== undefined ? timeout : configured !== undefined ? configured : fallback;\n\n const finalHeaders: Record<string, string> = {\n ...(isForm ? {} : { \"Content-Type\": \"application/json\" }),\n ...(requestId ? { \"X-Request-ID\": requestId } : {}),\n ...config.headers,\n ...authHeaders(),\n ...(headers as Record<string, string> | undefined),\n };\n\n const timed = withTimeout(signal, limit);\n const init: RequestInit = {\n ...rest,\n signal: timed.signal,\n headers: finalHeaders,\n credentials: config.withCredentials ? \"include\" : rest.credentials,\n body:\n body === undefined || body === null\n ? undefined\n : isForm\n ? (body as FormData)\n : JSON.stringify(body),\n };\n\n const url = buildApiUrl(config.baseURL, path, { prefix: config.prefix, params });\n try {\n return await fetcher(url, init);\n } catch (cause) {\n if (timed.timedOut()) {\n throw new TempestApiError({\n status: 0,\n detail: `A requisição excedeu ${limit}ms e foi abandonada.`,\n });\n }\n throw cause;\n } finally {\n timed.dispose();\n }\n }\n\n async function send(\n path: string,\n options: RequestOptions,\n requestId: string,\n method: string,\n ): Promise<Response> {\n const log = config.logger;\n if (!log) return rawRequest(path, options, requestId);\n\n const startedAt = performance.now();\n try {\n const response = await rawRequest(path, options, requestId);\n const entry = { requestId, status: response.status, ms: elapsedMs(startedAt) };\n const line = `${method} ${path} → ${response.status}`;\n if (response.status >= 400) log.warn(line, entry);\n else log.debug(line, entry);\n return response;\n } catch (error) {\n log.warn(`${method} ${path} → no response`, {\n requestId,\n ms: elapsedMs(startedAt),\n error,\n });\n throw error;\n }\n }\n\n async function notifyUnauthorized(response: Response, requestId: string): Promise<void> {\n if (!config.onUnauthorized) return;\n config.logger?.warn(`unauthorized — calling onUnauthorized`, {\n requestId,\n status: response.status,\n });\n try {\n await config.onUnauthorized(response);\n } catch (error) {\n config.logger?.warn(`onUnauthorized threw — keeping the original response error`, {\n requestId,\n status: response.status,\n error,\n });\n }\n }\n\n async function attempt<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n const method = (options.method ?? \"GET\").toUpperCase();\n let response = await send(path, options, requestId, method);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await send(path, options, requestId, method);\n } catch {\n await notifyUnauthorized(response, requestId);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await notifyUnauthorized(response, requestId);\n }\n } else {\n await notifyUnauthorized(response, requestId);\n }\n }\n\n if (!response.ok) {\n throw await parseError(response, requestId);\n }\n\n return decode(response);\n }\n\n async function run<T>(\n path: string,\n options: RequestOptions,\n decode: ResponseDecoder<T>,\n ): Promise<T> {\n const retryOptions = resolveRetry(config.retry);\n if (!retryOptions) return attempt<T>(path, options, decode);\n\n const method = (options.method ?? \"GET\").toUpperCase();\n return retryWithBackoff(() => attempt<T>(path, options, decode), {\n ...retryOptions,\n shouldRetry:\n retryOptions.shouldRetry ?? ((error: unknown) => isRetriableFailure(error, method)),\n });\n }\n\n async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return run<T>(path, options, decodeByContentType);\n }\n\n async function upload<T>(\n path: string,\n formData: FormData,\n method: \"POST\" | \"PUT\" | \"PATCH\" = \"POST\",\n options?: Omit<RequestOptions, \"body\" | \"method\">,\n ): Promise<T> {\n return request<T>(path, { ...options, method, body: formData });\n }\n\n return {\n request,\n get: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"GET\" }),\n post: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"POST\" }),\n put: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PUT\" }),\n patch: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"PATCH\" }),\n delete: <T>(path: string, options?: RequestOptions) =>\n request<T>(path, { ...options, method: \"DELETE\" }),\n blob: (path: string, options?: RequestOptions) =>\n run<Blob>(path, { ...options }, (response) => response.blob()),\n arrayBuffer: (path: string, options?: RequestOptions) =>\n run<ArrayBuffer>(path, { ...options }, (response) => response.arrayBuffer()),\n upload,\n };\n}\n"],"mappings":"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"}
|