tempest-react-sdk 0.43.0 → 0.45.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 +12 -5
- package/dist/app/AppProviders.cjs +1 -1
- package/dist/app/AppProviders.js +6 -6
- package/dist/audio/sfx-pool.cjs +2 -0
- package/dist/audio/sfx-pool.cjs.map +1 -0
- package/dist/audio/sfx-pool.js +62 -0
- package/dist/audio/sfx-pool.js.map +1 -0
- package/dist/audio/use-sfx-pool.cjs +2 -0
- package/dist/audio/use-sfx-pool.cjs.map +1 -0
- package/dist/audio/use-sfx-pool.js +18 -0
- package/dist/audio/use-sfx-pool.js.map +1 -0
- package/dist/auth/create-tempest-auth.cjs +1 -1
- package/dist/auth/create-tempest-auth.cjs.map +1 -1
- package/dist/auth/create-tempest-auth.js +47 -44
- package/dist/auth/create-tempest-auth.js.map +1 -1
- package/dist/auth/lazy-with-retry.cjs +1 -1
- package/dist/auth/lazy-with-retry.cjs.map +1 -1
- package/dist/auth/lazy-with-retry.js +8 -1
- package/dist/auth/lazy-with-retry.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js +27 -25
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AppBar/use-sticky-body-warning.cjs +2 -0
- package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -0
- package/dist/components/AppBar/use-sticky-body-warning.js +16 -0
- package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -0
- package/dist/components/BarList/BarList.cjs +2 -0
- package/dist/components/BarList/BarList.cjs.map +1 -0
- package/dist/components/BarList/BarList.js +42 -0
- package/dist/components/BarList/BarList.js.map +1 -0
- package/dist/components/BarList/BarList.module.cjs +2 -0
- package/dist/components/BarList/BarList.module.cjs.map +1 -0
- package/dist/components/BarList/BarList.module.js +15 -0
- package/dist/components/BarList/BarList.module.js.map +1 -0
- package/dist/components/BarList/bar-list-model.cjs +2 -0
- package/dist/components/BarList/bar-list-model.cjs.map +1 -0
- package/dist/components/BarList/bar-list-model.js +18 -0
- package/dist/components/BarList/bar-list-model.js.map +1 -0
- package/dist/components/DataTable/DataTable.cjs +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js +141 -108
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js +13 -10
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/LoadingRows.cjs +2 -0
- package/dist/components/DataTable/LoadingRows.cjs.map +1 -0
- package/dist/components/DataTable/LoadingRows.js +18 -0
- package/dist/components/DataTable/LoadingRows.js.map +1 -0
- package/dist/components/DataTable/use-dev-warnings.cjs +2 -0
- package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -0
- package/dist/components/DataTable/use-dev-warnings.js +17 -0
- package/dist/components/DataTable/use-dev-warnings.js.map +1 -0
- package/dist/components/FilterBar/filter-apply.cjs +2 -0
- package/dist/components/FilterBar/filter-apply.cjs.map +1 -0
- package/dist/components/FilterBar/filter-apply.js +76 -0
- package/dist/components/FilterBar/filter-apply.js.map +1 -0
- package/dist/components/FilterBar/filter-query.cjs +2 -0
- package/dist/components/FilterBar/filter-query.cjs.map +1 -0
- package/dist/components/FilterBar/filter-query.js +38 -0
- package/dist/components/FilterBar/filter-query.js.map +1 -0
- package/dist/hooks/use-countdown.cjs +2 -0
- package/dist/hooks/use-countdown.cjs.map +1 -0
- package/dist/hooks/use-countdown.js +18 -0
- package/dist/hooks/use-countdown.js.map +1 -0
- package/dist/hooks/use-latest-ref.cjs +2 -0
- package/dist/hooks/use-latest-ref.cjs.map +1 -0
- package/dist/hooks/use-latest-ref.js +10 -0
- package/dist/hooks/use-latest-ref.js.map +1 -0
- package/dist/hooks/use-typewriter.cjs +2 -0
- package/dist/hooks/use-typewriter.cjs.map +1 -0
- package/dist/hooks/use-typewriter.js +28 -0
- package/dist/hooks/use-typewriter.js.map +1 -0
- package/dist/http/api-client.cjs +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js +42 -43
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/build-url.cjs +2 -0
- package/dist/http/build-url.cjs.map +1 -0
- package/dist/http/build-url.js +26 -0
- package/dist/http/build-url.js.map +1 -0
- package/dist/http/describe-api-error.cjs +2 -0
- package/dist/http/describe-api-error.cjs.map +1 -0
- package/dist/http/describe-api-error.js +22 -0
- package/dist/http/describe-api-error.js.map +1 -0
- package/dist/http/use-describe-api-error.cjs +2 -0
- package/dist/http/use-describe-api-error.cjs.map +1 -0
- package/dist/http/use-describe-api-error.js +15 -0
- package/dist/http/use-describe-api-error.js.map +1 -0
- package/dist/i18n/I18nProvider.cjs +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/i18n/I18nProvider.js +4 -1
- package/dist/i18n/I18nProvider.js.map +1 -1
- package/dist/query/QueryProvider.cjs +1 -1
- package/dist/query/QueryProvider.cjs.map +1 -1
- package/dist/query/QueryProvider.js +13 -12
- package/dist/query/QueryProvider.js.map +1 -1
- package/dist/query/retry-policy.cjs +2 -0
- package/dist/query/retry-policy.cjs.map +1 -0
- package/dist/query/retry-policy.js +10 -0
- package/dist/query/retry-policy.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +867 -10
- package/dist/tempest-react-sdk.js +276 -261
- package/dist/utils/compressed-storage.cjs +2 -0
- package/dist/utils/compressed-storage.cjs.map +1 -0
- package/dist/utils/compressed-storage.js +52 -0
- package/dist/utils/compressed-storage.js.map +1 -0
- package/dist/utils/csv.cjs +4 -0
- package/dist/utils/csv.cjs.map +1 -0
- package/dist/utils/csv.js +25 -0
- package/dist/utils/csv.js.map +1 -0
- package/dist/utils/format.cjs +1 -1
- package/dist/utils/format.cjs.map +1 -1
- package/dist/utils/format.js +11 -4
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/numbers.cjs +1 -1
- package/dist/utils/numbers.cjs.map +1 -1
- package/dist/utils/numbers.js +5 -2
- package/dist/utils/numbers.js.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
- package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
- package/dist/vite/tempest-pwa-dev-sw.js +10 -5
- package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
- package/dist/vite/tempest-pwa-manifest.cjs +2 -2
- package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
- package/dist/vite/tempest-pwa-manifest.js +17 -13
- package/dist/vite/tempest-pwa-manifest.js.map +1 -1
- package/dist/vite.d.ts +19 -2
- package/package.json +1 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Skeleton as e } from "../Skeleton/Skeleton.js";
|
|
2
|
+
import t from "./DataTable.module.js";
|
|
3
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
4
|
+
//#region src/components/DataTable/LoadingRows.tsx
|
|
5
|
+
function r({ columns: r, rows: i }) {
|
|
6
|
+
return /* @__PURE__ */ n("div", {
|
|
7
|
+
className: t.loading,
|
|
8
|
+
"aria-hidden": "true",
|
|
9
|
+
children: Array.from({ length: i }, (i, a) => /* @__PURE__ */ n("div", {
|
|
10
|
+
className: t.loadingRow,
|
|
11
|
+
children: Array.from({ length: Math.max(r, 1) }, (t, r) => /* @__PURE__ */ n(e, { variant: "text" }, r))
|
|
12
|
+
}, a))
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { r as LoadingRows };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=LoadingRows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoadingRows.js","names":[],"sources":["../../../src/components/DataTable/LoadingRows.tsx"],"sourcesContent":["import { Skeleton } from \"../Skeleton\";\nimport styles from \"./DataTable.module.css\";\n\n/** Props for {@link LoadingRows}. */\nexport interface LoadingRowsProps {\n /** How many placeholder cells to draw per row. */\n columns: number;\n /** How many placeholder rows to draw. */\n rows: number;\n}\n\n/**\n * Placeholder rows for a table that has nothing to show yet.\n *\n * A first load and an empty result are different statements, and the usual\n * shortcut — showing `emptyMessage` while the request is in flight — tells the\n * user \"there is nothing here\" and then contradicts itself a moment later.\n * Drawing rows at roughly the real height also keeps whatever sits below the\n * table from jumping when the data lands.\n *\n * The whole block is `aria-hidden`: it carries no information a screen reader can\n * use, and the busy state is already on the wrapper. It also stays out of\n * `role=\"status\"`, which the table's announcement region already occupies —\n * two status regions on one table means the second one is what gets ignored.\n *\n * @param props - Grid size to fill.\n * @returns The placeholder block.\n */\nexport function LoadingRows({ columns, rows }: LoadingRowsProps) {\n return (\n <div className={styles.loading} aria-hidden=\"true\">\n {Array.from({ length: rows }, (_, rowIndex) => (\n <div key={rowIndex} className={styles.loadingRow}>\n {Array.from({ length: Math.max(columns, 1) }, (_, cellIndex) => (\n <Skeleton key={cellIndex} variant=\"text\" />\n ))}\n </div>\n ))}\n </div>\n );\n}\n"],"mappings":";;;;AA4BA,SAAgB,EAAY,EAAE,YAAS,WAA0B;CAC7D,OACI,kBAAC,OAAD;EAAK,WAAW,EAAO;EAAS,eAAY;EACvC,UAAA,MAAM,KAAK,EAAE,QAAQ,EAAK,IAAI,GAAG,MAC9B,kBAAC,OAAD;GAAoB,WAAW,EAAO;GACjC,UAAA,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAS,CAAC,EAAE,IAAI,GAAG,MAC9C,kBAAC,GAAD,EAA0B,SAAQ,OAAQ,GAA3B,CAA2B,CAC7C;EACA,GAJK,CAIL,CACR;CACA,CAAA;AAEb"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react");function t({serverMode:t,controlledPage:n,onPageChange:r,sortIsManual:i,onSortChange:a}){(0,e.useEffect)(()=>{process.env.NODE_ENV!==`production`&&(t&&n===void 0&&console.warn("[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. Without it the pager moves the internal page while `data` keeps showing page 1."),n!==void 0&&!r&&console.warn("[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything."),i&&!a&&console.warn("[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else."))},[t,n,r,i,a])}exports.useDevWarnings=t;
|
|
2
|
+
//# sourceMappingURL=use-dev-warnings.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-dev-warnings.cjs","names":[],"sources":["../../../src/components/DataTable/use-dev-warnings.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\n/** What {@link useDevWarnings} needs to judge the prop combination. */\nexport interface DataTableDevWarningsInput {\n /** Whether `totalItems` was passed. */\n serverMode: boolean;\n /** The controlled `page`, if any. */\n controlledPage: number | undefined;\n /** The page callback, if any. */\n onPageChange: ((page: number) => void) | undefined;\n /** Whether sorting is delegated. */\n sortIsManual: boolean;\n /** The sort callback, if any. */\n onSortChange: ((sort: never) => void) | undefined;\n}\n\n/**\n * Warn, in development only, about prop combinations that render a table which\n * silently lies.\n *\n * Each of these produces a screen that looks like it works: the header sorts and\n * nothing moves, the pager renders and clicking it does nothing. They are not\n * type errors — every prop is optional on its own — so the only place to catch\n * them is at runtime, once, in dev.\n *\n * @param input - The resolved prop combination.\n */\nexport function useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n onSortChange,\n}: DataTableDevWarningsInput): void {\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n\n if (serverMode && controlledPage === undefined) {\n console.warn(\n \"[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. \" +\n \"Without it the pager moves the internal page while `data` keeps showing page 1.\",\n );\n }\n if (controlledPage !== undefined && !onPageChange) {\n console.warn(\n \"[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything.\",\n );\n }\n if (sortIsManual && !onSortChange) {\n console.warn(\n \"[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else.\",\n );\n }\n }, [serverMode, controlledPage, onPageChange, sortIsManual, onSortChange]);\n}\n"],"mappings":"uBA2BA,SAAgB,EAAe,CAC3B,aACA,iBACA,eACA,eACA,gBACgC,EAChC,EAAA,EAAA,UAAA,KAAgB,CACZ,QAAA,IAAA,WAA6B,eAEzB,GAAc,IAAmB,IAAA,IACjC,QAAQ,KACJ,mKAEJ,EAEA,IAAmB,IAAA,IAAa,CAAC,GACjC,QAAQ,KACJ,0GACJ,EAEA,GAAgB,CAAC,GACjB,QAAQ,KACJ,yIACJ,EAER,EAAG,CAAC,EAAY,EAAgB,EAAc,EAAc,CAAY,CAAC,CAC7E"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect as e } from "react";
|
|
2
|
+
//#region src/components/DataTable/use-dev-warnings.ts
|
|
3
|
+
function t({ serverMode: t, controlledPage: n, onPageChange: r, sortIsManual: i, onSortChange: a }) {
|
|
4
|
+
e(() => {
|
|
5
|
+
process.env.NODE_ENV !== "production" && (t && n === void 0 && console.warn("[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. Without it the pager moves the internal page while `data` keeps showing page 1."), n !== void 0 && !r && console.warn("[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything."), i && !a && console.warn("[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else."));
|
|
6
|
+
}, [
|
|
7
|
+
t,
|
|
8
|
+
n,
|
|
9
|
+
r,
|
|
10
|
+
i,
|
|
11
|
+
a
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { t as useDevWarnings };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=use-dev-warnings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-dev-warnings.js","names":[],"sources":["../../../src/components/DataTable/use-dev-warnings.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\n/** What {@link useDevWarnings} needs to judge the prop combination. */\nexport interface DataTableDevWarningsInput {\n /** Whether `totalItems` was passed. */\n serverMode: boolean;\n /** The controlled `page`, if any. */\n controlledPage: number | undefined;\n /** The page callback, if any. */\n onPageChange: ((page: number) => void) | undefined;\n /** Whether sorting is delegated. */\n sortIsManual: boolean;\n /** The sort callback, if any. */\n onSortChange: ((sort: never) => void) | undefined;\n}\n\n/**\n * Warn, in development only, about prop combinations that render a table which\n * silently lies.\n *\n * Each of these produces a screen that looks like it works: the header sorts and\n * nothing moves, the pager renders and clicking it does nothing. They are not\n * type errors — every prop is optional on its own — so the only place to catch\n * them is at runtime, once, in dev.\n *\n * @param input - The resolved prop combination.\n */\nexport function useDevWarnings({\n serverMode,\n controlledPage,\n onPageChange,\n sortIsManual,\n onSortChange,\n}: DataTableDevWarningsInput): void {\n useEffect(() => {\n if (process.env.NODE_ENV === \"production\") return;\n\n if (serverMode && controlledPage === undefined) {\n console.warn(\n \"[tempest] <DataTable totalItems> is server mode, which needs a controlled `page`. \" +\n \"Without it the pager moves the internal page while `data` keeps showing page 1.\",\n );\n }\n if (controlledPage !== undefined && !onPageChange) {\n console.warn(\n \"[tempest] <DataTable page> is controlled but `onPageChange` is missing, so the pager cannot do anything.\",\n );\n }\n if (sortIsManual && !onSortChange) {\n console.warn(\n \"[tempest] <DataTable> is sorting manually but `onSortChange` is missing: clicking a sortable header changes the arrow and nothing else.\",\n );\n }\n }, [serverMode, controlledPage, onPageChange, sortIsManual, onSortChange]);\n}\n"],"mappings":";;AA2BA,SAAgB,EAAe,EAC3B,eACA,mBACA,iBACA,iBACA,mBACgC;CAChC,QAAgB;EACZ,QAAA,IAAA,aAA6B,iBAEzB,KAAc,MAAmB,KAAA,KACjC,QAAQ,KACJ,mKAEJ,GAEA,MAAmB,KAAA,KAAa,CAAC,KACjC,QAAQ,KACJ,0GACJ,GAEA,KAAgB,CAAC,KACjB,QAAQ,KACJ,yIACJ;CAER,GAAG;EAAC;EAAY;EAAgB;EAAc;EAAc;CAAY,CAAC;AAC7E"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./filter-model.cjs");var t=/^\d{4}-\d{2}-\d{2}$/;function n(e,t){if(!(typeof e!=`object`||!e))return e[t]}function r(e){if(typeof e==`string`&&t.test(e))return e;let n=e instanceof Date?e:typeof e==`string`&&e.trim()!==``?new Date(e):null;if(n===null||Number.isNaN(n.getTime()))return null;let r=`${n.getMonth()+1}`.padStart(2,`0`),i=`${n.getDate()}`.padStart(2,`0`);return`${n.getFullYear()}-${r}-${i}`}function i(e,n){if(e==null)return null;if(typeof e==`number`){let t=Number(n);return Number.isNaN(t)?null:e-t}if(typeof e==`boolean`){let t=n===`true`||n!==`false`&&null;return t===null?null:Number(e)-Number(t)}if(t.test(n)){let t=r(e);return t===null?null:t.localeCompare(n)}return String(e).localeCompare(n,void 0,{numeric:!0})}function a(e,t){let n=i(e,t);return n===null?String(e??``)===t:n===0}function o(e){return e==null?!0:typeof e==`string`?e.trim()===``:Array.isArray(e)?e.length===0:!1}function s(e){return e.value===void 0?[]:Array.isArray(e.value)?[...e.value]:[String(e.value)]}function c(e,t,n){switch(e){case`empty`:return o(t);case`notEmpty`:return!o(t);case`eq`:return a(t,n[0]??``);case`ne`:return!a(t,n[0]??``);case`contains`:return String(t??``).toLowerCase().includes((n[0]??``).toLowerCase());case`in`:return n.some(e=>a(t,e));case`between`:{let[e=``,r=``]=n,a=i(t,e),o=i(t,r);return a===null||o===null?!1:a>=0&&o<=0}default:{let r=i(t,n[0]??``);return r===null?!1:e===`gt`?r>0:e===`gte`?r>=0:e===`lt`?r<0:r<=0}}}function l(e){let[n=``,r=``]=e;if(t.test(n)&&t.test(r))return n<=r?[n,r]:[r,n];let i=Number(n),a=Number(r);return!Number.isNaN(i)&&!Number.isNaN(a)?i<=a?[n,r]:[r,n]:n.localeCompare(r,void 0,{numeric:!0})<=0?[n,r]:[r,n]}function u(t,r){let i=r.filter(e.isComplete);return i.length===0?[...t]:t.filter(t=>i.every(r=>{let i=n(t,r.field);if(e.isValueless(r.operator))return c(r.operator,i,[]);let a=r.operator===`between`?l(s(r)):s(r);return c(r.operator,i,a)}))}exports.applyFilters=u,exports.orderRange=l;
|
|
2
|
+
//# sourceMappingURL=filter-apply.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-apply.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete, isValueless } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Built from the local calendar parts rather than `toISOString`, which converts\n * to UTC first and therefore reports the next day for anything after 21:00 in\n * UTC-3. A `yyyy-mm-dd` string is returned untouched, so a filter value and a\n * row value that already agree never round-trip through `Date` at all.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (typeof value === \"string\" && DATE_ONLY.test(value)) return value;\n\n const date =\n value instanceof Date\n ? value\n : typeof value === \"string\" && value.trim() !== \"\"\n ? new Date(value)\n : null;\n if (date === null || Number.isNaN(date.getTime())) return null;\n\n const month = `${date.getMonth() + 1}`.padStart(2, \"0\");\n const day = `${date.getDate()}`.padStart(2, \"0\");\n return `${date.getFullYear()}-${month}-${day}`;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return String(left).localeCompare(right, undefined, { numeric: true });\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return first.localeCompare(second, undefined, { numeric: true }) <= 0\n ? [first, second]\n : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete);\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every((filter) => {\n const left = readField(item, filter.field);\n if (isValueless(filter.operator)) {\n return matchesOperator(filter.operator, left, []);\n }\n const values =\n filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter);\n return matchesOperator(filter.operator, left, values);\n }),\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isComplete as e, isValueless as t } from "./filter-model.js";
|
|
2
|
+
//#region src/components/FilterBar/filter-apply.ts
|
|
3
|
+
var n = /^\d{4}-\d{2}-\d{2}$/;
|
|
4
|
+
function r(e, t) {
|
|
5
|
+
if (!(typeof e != "object" || !e)) return e[t];
|
|
6
|
+
}
|
|
7
|
+
function i(e) {
|
|
8
|
+
if (typeof e == "string" && n.test(e)) return e;
|
|
9
|
+
let t = e instanceof Date ? e : typeof e == "string" && e.trim() !== "" ? new Date(e) : null;
|
|
10
|
+
if (t === null || Number.isNaN(t.getTime())) return null;
|
|
11
|
+
let r = `${t.getMonth() + 1}`.padStart(2, "0"), i = `${t.getDate()}`.padStart(2, "0");
|
|
12
|
+
return `${t.getFullYear()}-${r}-${i}`;
|
|
13
|
+
}
|
|
14
|
+
function a(e, t) {
|
|
15
|
+
if (e == null) return null;
|
|
16
|
+
if (typeof e == "number") {
|
|
17
|
+
let n = Number(t);
|
|
18
|
+
return Number.isNaN(n) ? null : e - n;
|
|
19
|
+
}
|
|
20
|
+
if (typeof e == "boolean") {
|
|
21
|
+
let n = t === "true" || t !== "false" && null;
|
|
22
|
+
return n === null ? null : Number(e) - Number(n);
|
|
23
|
+
}
|
|
24
|
+
if (n.test(t)) {
|
|
25
|
+
let n = i(e);
|
|
26
|
+
return n === null ? null : n.localeCompare(t);
|
|
27
|
+
}
|
|
28
|
+
return String(e).localeCompare(t, void 0, { numeric: !0 });
|
|
29
|
+
}
|
|
30
|
+
function o(e, t) {
|
|
31
|
+
let n = a(e, t);
|
|
32
|
+
return n === null ? String(e ?? "") === t : n === 0;
|
|
33
|
+
}
|
|
34
|
+
function s(e) {
|
|
35
|
+
return e == null ? !0 : typeof e == "string" ? e.trim() === "" : Array.isArray(e) ? e.length === 0 : !1;
|
|
36
|
+
}
|
|
37
|
+
function c(e) {
|
|
38
|
+
return e.value === void 0 ? [] : Array.isArray(e.value) ? [...e.value] : [String(e.value)];
|
|
39
|
+
}
|
|
40
|
+
function l(e, t, n) {
|
|
41
|
+
switch (e) {
|
|
42
|
+
case "empty": return s(t);
|
|
43
|
+
case "notEmpty": return !s(t);
|
|
44
|
+
case "eq": return o(t, n[0] ?? "");
|
|
45
|
+
case "ne": return !o(t, n[0] ?? "");
|
|
46
|
+
case "contains": return String(t ?? "").toLowerCase().includes((n[0] ?? "").toLowerCase());
|
|
47
|
+
case "in": return n.some((e) => o(t, e));
|
|
48
|
+
case "between": {
|
|
49
|
+
let [e = "", r = ""] = n, i = a(t, e), o = a(t, r);
|
|
50
|
+
return i === null || o === null ? !1 : i >= 0 && o <= 0;
|
|
51
|
+
}
|
|
52
|
+
default: {
|
|
53
|
+
let r = a(t, n[0] ?? "");
|
|
54
|
+
return r === null ? !1 : e === "gt" ? r > 0 : e === "gte" ? r >= 0 : e === "lt" ? r < 0 : r <= 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function u(e) {
|
|
59
|
+
let [t = "", r = ""] = e;
|
|
60
|
+
if (n.test(t) && n.test(r)) return t <= r ? [t, r] : [r, t];
|
|
61
|
+
let i = Number(t), a = Number(r);
|
|
62
|
+
return !Number.isNaN(i) && !Number.isNaN(a) ? i <= a ? [t, r] : [r, t] : t.localeCompare(r, void 0, { numeric: !0 }) <= 0 ? [t, r] : [r, t];
|
|
63
|
+
}
|
|
64
|
+
function d(n, i) {
|
|
65
|
+
let a = i.filter(e);
|
|
66
|
+
return a.length === 0 ? [...n] : n.filter((e) => a.every((n) => {
|
|
67
|
+
let i = r(e, n.field);
|
|
68
|
+
if (t(n.operator)) return l(n.operator, i, []);
|
|
69
|
+
let a = n.operator === "between" ? u(c(n)) : c(n);
|
|
70
|
+
return l(n.operator, i, a);
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { d as applyFilters, u as orderRange };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=filter-apply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-apply.js","names":[],"sources":["../../../src/components/FilterBar/filter-apply.ts"],"sourcesContent":["// The other half of the filter model: evaluating a `Filter[]` instead of only\n// describing it. `FilterBar` produces filters, `applyFilters` runs them over an\n// in-memory list, and `filtersToQueryParams` (filter-query.ts) hands the same set\n// to a paginated backend. Without these two the model stops one step short of\n// useful and every app writes the eleven operator branches again.\n\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete, isValueless } from \"./filter-model\";\n\n/** Matches a plain `yyyy-mm-dd`, the shape `<input type=\"date\">` produces. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * Read a property off a row without asserting the row's shape.\n *\n * `Filter.field` is a string chosen by the app's field list, so it cannot be a\n * `keyof T`; the lookup goes through an index signature and a missing property\n * yields `undefined`, which the operators treat as an empty value.\n *\n * @param item - The row being tested.\n * @param field - Property name from the filter.\n * @returns The property value, or `undefined` when the row has no such key.\n */\nfunction readField(item: unknown, field: string): unknown {\n if (item == null || typeof item !== \"object\") return undefined;\n return (item as Record<string, unknown>)[field];\n}\n\n/**\n * Local `yyyy-mm-dd` for a value that represents a date, or `null`.\n *\n * Built from the local calendar parts rather than `toISOString`, which converts\n * to UTC first and therefore reports the next day for anything after 21:00 in\n * UTC-3. A `yyyy-mm-dd` string is returned untouched, so a filter value and a\n * row value that already agree never round-trip through `Date` at all.\n *\n * @param value - A `Date`, a date-ish string, or anything else.\n * @returns The day key, or `null` when the value does not denote a date.\n */\nfunction dayKey(value: unknown): string | null {\n if (typeof value === \"string\" && DATE_ONLY.test(value)) return value;\n\n const date =\n value instanceof Date\n ? value\n : typeof value === \"string\" && value.trim() !== \"\"\n ? new Date(value)\n : null;\n if (date === null || Number.isNaN(date.getTime())) return null;\n\n const month = `${date.getMonth() + 1}`.padStart(2, \"0\");\n const day = `${date.getDate()}`.padStart(2, \"0\");\n return `${date.getFullYear()}-${month}-${day}`;\n}\n\n/**\n * Compare a row value against a filter value, in the row value's own type.\n *\n * The filter always carries strings — it comes from form inputs and from the\n * URL — while the row carries whatever the API returned. Comparing them as text\n * gets the two common cases wrong: `10` would sort before `9`, and a date would\n * be compared by however it happens to be formatted. So the row value picks the\n * comparison, and the filter value is coerced into it.\n *\n * Dates compare by **day**, not by instant: a row stamped `2026-03-05T13:00:00Z`\n * is `>= 2026-03-05`, which is what the person picking a date in the UI means.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Negative, zero or positive, or `null` when the two cannot be compared.\n */\nfunction compare(left: unknown, right: string): number | null {\n if (left == null) return null;\n\n if (typeof left === \"number\") {\n const parsed = Number(right);\n return Number.isNaN(parsed) ? null : left - parsed;\n }\n\n if (typeof left === \"boolean\") {\n const parsed = right === \"true\" ? true : right === \"false\" ? false : null;\n return parsed === null ? null : Number(left) - Number(parsed);\n }\n\n if (DATE_ONLY.test(right)) {\n const key = dayKey(left);\n return key === null ? null : key.localeCompare(right);\n }\n\n return String(left).localeCompare(right, undefined, { numeric: true });\n}\n\n/**\n * Equality in the row value's type, falling back to exact text.\n *\n * Case-sensitive on purpose: `eq` maps to a plain `WHERE column = value` on the\n * server side, so a case-insensitive client would quietly disagree with the\n * backend for the same filter. `contains` is the case-insensitive operator, and\n * it is also the default one for text fields, so the friendly behaviour is what\n * people get without asking.\n *\n * @param left - The row value.\n * @param right - The raw filter value.\n * @returns Whether the two are equal.\n */\nfunction equals(left: unknown, right: string): boolean {\n const result = compare(left, right);\n if (result !== null) return result === 0;\n return String(left ?? \"\") === right;\n}\n\n/**\n * Is this row value empty?\n *\n * Empty means absent, blank text or an empty list — never falsy. `0` and `false`\n * are values somebody chose, and a filter that hid them would be reporting the\n * wrong count on every dashboard that tracks zeroes.\n *\n * @param value - The row value.\n * @returns Whether the value counts as empty.\n */\nfunction isEmptyValue(value: unknown): boolean {\n if (value == null) return true;\n if (typeof value === \"string\") return value.trim() === \"\";\n if (Array.isArray(value)) return value.length === 0;\n return false;\n}\n\n/**\n * The filter's value as a list, whatever arity the operator uses.\n *\n * @param filter - The filter to read.\n * @returns Its values, trimmed of nothing — a leading space may be meaningful text.\n */\nfunction valuesOf(filter: Filter): string[] {\n if (filter.value === undefined) return [];\n return Array.isArray(filter.value) ? [...filter.value] : [String(filter.value)];\n}\n\n/**\n * Evaluate one operator against one row value.\n *\n * @param operator - The comparison to make.\n * @param left - The row value.\n * @param values - The filter values (one, two for `between`, many for `in`).\n * @returns Whether the row value satisfies the operator.\n */\nfunction matchesOperator(operator: FilterOperator, left: unknown, values: string[]): boolean {\n switch (operator) {\n case \"empty\":\n return isEmptyValue(left);\n case \"notEmpty\":\n return !isEmptyValue(left);\n case \"eq\":\n return equals(left, values[0] ?? \"\");\n case \"ne\":\n return !equals(left, values[0] ?? \"\");\n case \"contains\":\n return String(left ?? \"\")\n .toLowerCase()\n .includes((values[0] ?? \"\").toLowerCase());\n case \"in\":\n return values.some((value) => equals(left, value));\n case \"between\": {\n const [first = \"\", second = \"\"] = values;\n const low = compare(left, first);\n const high = compare(left, second);\n if (low === null || high === null) return false;\n return low >= 0 && high <= 0;\n }\n default: {\n const result = compare(left, values[0] ?? \"\");\n if (result === null) return false;\n if (operator === \"gt\") return result > 0;\n if (operator === \"gte\") return result >= 0;\n if (operator === \"lt\") return result < 0;\n return result <= 0;\n }\n }\n}\n\n/**\n * Order the two values of a `between` so the range is never empty by accident.\n *\n * A person who picks the later date first means the range between the two dates,\n * not \"no rows\". The server-side `BETWEEN` needs `(lo, hi)` in order too, which\n * is why the normalisation lives here and is shared by `filtersToQueryParams`.\n *\n * @param values - The raw pair.\n * @returns The pair, ascending.\n */\nexport function orderRange(values: readonly string[]): string[] {\n const [first = \"\", second = \"\"] = values;\n if (DATE_ONLY.test(first) && DATE_ONLY.test(second)) {\n return first <= second ? [first, second] : [second, first];\n }\n const low = Number(first);\n const high = Number(second);\n if (!Number.isNaN(low) && !Number.isNaN(high)) {\n return low <= high ? [first, second] : [second, first];\n }\n return first.localeCompare(second, undefined, { numeric: true }) <= 0\n ? [first, second]\n : [second, first];\n}\n\n/**\n * Run a filter set over an in-memory list.\n *\n * Closes the loop `FilterBar` opens: the bar produces `Filter[]`, this applies\n * them. Filters combine with `AND`, matching the flat model the bar builds, and\n * an incomplete filter is skipped rather than treated as a match of nothing — a\n * half-filled form should not empty the table underneath it.\n *\n * Comparison follows the row's type, not the filter's: numbers compare\n * numerically, dates compare by day, and everything else compares as text with\n * `numeric: true` so `\"item 2\"` lands before `\"item 10\"`.\n *\n * A few behaviours differ from the SQL the server-side twin produces, and the\n * difference is deliberate rather than accidental:\n *\n * - `ne` matches rows whose value is absent. In SQL, `column <> 'x'` is `NULL`\n * for a `NULL` column and the row drops out. Here \"is not paid\" shows the rows\n * with no status at all, which is what the chip claims.\n * - `empty` matches `NULL`, blank text and empty lists; `__isnull` on the server\n * only matches `NULL`. A column that stores `\"\"` instead of `NULL` is where\n * the two disagree.\n *\n * @example\n * const visible = applyFilters(orders, filters);\n *\n * @param items - The full list.\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns A new array with the rows that satisfy every complete filter.\n */\nexport function applyFilters<T>(items: readonly T[], filters: readonly Filter[]): T[] {\n const active = filters.filter(isComplete);\n if (active.length === 0) return [...items];\n\n return items.filter((item) =>\n active.every((filter) => {\n const left = readField(item, filter.field);\n if (isValueless(filter.operator)) {\n return matchesOperator(filter.operator, left, []);\n }\n const values =\n filter.operator === \"between\" ? orderRange(valuesOf(filter)) : valuesOf(filter);\n return matchesOperator(filter.operator, left, values);\n }),\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./filter-model.cjs"),t=require("./filter-apply.cjs");var n={eq:``,ne:`__ne`,contains:`__icontains`,gt:`__gt`,gte:`__gte`,lt:`__lt`,lte:`__lte`,between:`__between`,in:`__in`,empty:`__isnull`,notEmpty:`__isnull`},r=`name`;function i(i){let a=new URLSearchParams;for(let o of i){if(!e.isComplete(o))continue;let i=n[o.operator],s=o.operator===`eq`&&o.field===r?`${o.field}__iexact`:`${o.field}${i}`;if(o.operator===`empty`||o.operator===`notEmpty`){a.append(s,o.operator===`empty`?`true`:`false`);continue}let c=Array.isArray(o.value)?[...o.value]:[String(o.value??``)];if(o.operator===`between`){for(let e of t.orderRange(c))a.append(s,e);continue}for(let e of c)a.append(s,e)}return a}exports.filtersToQueryParams=i;
|
|
2
|
+
//# sourceMappingURL=filter-query.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-query.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-query.ts"],"sourcesContent":["// Server-side half of the filter model: the same `Filter[]` the bar produces,\n// encoded the way a paginated backend expects to read it. Kept apart from\n// `filtersToSearchParams` (filter-model.ts) because the two answer different\n// questions — that one round-trips the UI state through the URL, this one talks\n// to an API and its shape is the API's, not ours.\n\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\nimport { orderRange } from \"./filter-apply\";\n\n/**\n * Operator suffix per filter operator, following the `<column>__<op>` convention\n * of `tempest-fastapi-sdk`.\n *\n * Ported from `build_filter_condition` in\n * `tempest_fastapi_sdk/db/expressions.py`, which is what `BaseRepository`\n * dict filters and `Q` both resolve through. An empty suffix means the bare\n * column name, which that function resolves to plain equality.\n *\n * `contains` maps to `icontains` (case-insensitive, and the backend escapes the\n * `LIKE` wildcards) and both emptiness operators map to `isnull`, whose value\n * carries the direction.\n */\nconst OPERATOR_SUFFIX: Record<FilterOperator, string> = {\n eq: \"\",\n ne: \"__ne\",\n contains: \"__icontains\",\n gt: \"__gt\",\n gte: \"__gte\",\n lt: \"__lt\",\n lte: \"__lte\",\n between: \"__between\",\n in: \"__in\",\n empty: \"__isnull\",\n notEmpty: \"__isnull\",\n};\n\n/**\n * Column name the backend treats as a substring search when it carries no\n * operator suffix.\n *\n * Ported from `build_filter_condition`, which special-cases a string `name` into\n * `ILIKE %value%`. Sending a bare `name=` for an `eq` filter would therefore ask\n * for \"contains\" while the chip says \"is\", so `eq` on this column is emitted as\n * `name__iexact` instead.\n */\nconst SUBSTRING_COLUMN = \"name\";\n\n/**\n * Encode filters as query params for a paginated backend.\n *\n * The counterpart of {@link applyFilters}: same filter set, evaluated by the\n * database instead of by the browser, which is the only option once the list is\n * paginated on the server and the page in memory is not the whole result.\n *\n * The encoding is the `<column>__<op>` convention `tempest-fastapi-sdk` already\n * reads (`BaseRepository._apply_filters` → `build_filter_condition`):\n *\n * | Operator | Param |\n * | --- | --- |\n * | `eq` | `field` (or `field__iexact` for the `name` column) |\n * | `ne` | `field__ne` |\n * | `contains` | `field__icontains` |\n * | `gt` `gte` `lt` `lte` | `field__gt` … `field__lte` |\n * | `between` | `field__between` twice, low value first |\n * | `in` | `field__in` once per value |\n * | `empty` / `notEmpty` | `field__isnull=true` / `=false` |\n *\n * Returns `URLSearchParams` rather than a plain object because `between` carries\n * a pair and `in` carries a list: an object would keep only the last value of\n * each, silently narrowing the filter. Repeated params are also how FastAPI\n * receives a `list[str]` declared with `Query`.\n *\n * Two things the backend has to hold up its end of, or the filter fails quietly:\n *\n * - **Every key must be declared.** `BasePaginationFilterSchema.get_conditions()`\n * only forwards fields the subclass declares, so a `status__ne` the schema\n * never mentions is dropped by FastAPI before the repository sees it — no\n * error, no filtering.\n * - **`isnull` matches `NULL` only.** A column that stores `\"\"` for \"no value\"\n * will not answer an `empty` filter, while {@link applyFilters} treats blank\n * text as empty.\n *\n * @example\n * const params = filtersToQueryParams(filters);\n * params.set(\"page\", String(page));\n * const data = await api.get(`/orders?${params}`);\n *\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns Params ready to append to a request URL.\n */\nexport function filtersToQueryParams(filters: readonly Filter[]): URLSearchParams {\n const params = new URLSearchParams();\n\n for (const filter of filters) {\n if (!isComplete(filter)) continue;\n\n const suffix = OPERATOR_SUFFIX[filter.operator];\n const key =\n filter.operator === \"eq\" && filter.field === SUBSTRING_COLUMN\n ? `${filter.field}__iexact`\n : `${filter.field}${suffix}`;\n\n if (filter.operator === \"empty\" || filter.operator === \"notEmpty\") {\n params.append(key, filter.operator === \"empty\" ? \"true\" : \"false\");\n continue;\n }\n\n const values = Array.isArray(filter.value)\n ? [...filter.value]\n : [String(filter.value ?? \"\")];\n\n if (filter.operator === \"between\") {\n for (const value of orderRange(values)) params.append(key, value);\n continue;\n }\n\n for (const value of values) params.append(key, value);\n }\n\n return params;\n}\n"],"mappings":"sEAuBA,IAAM,EAAkD,CACpD,GAAI,GACJ,GAAI,OACJ,SAAU,cACV,GAAI,OACJ,IAAK,QACL,GAAI,OACJ,IAAK,QACL,QAAS,YACT,GAAI,OACJ,MAAO,WACP,SAAU,UACd,EAWM,EAAmB,OA6CzB,SAAgB,EAAqB,EAA6C,CAC9E,IAAM,EAAS,IAAI,gBAEnB,IAAK,IAAM,KAAU,EAAS,CAC1B,GAAI,CAAC,EAAA,WAAW,CAAM,EAAG,SAEzB,IAAM,EAAS,EAAgB,EAAO,UAChC,EACF,EAAO,WAAa,MAAQ,EAAO,QAAU,EACvC,GAAG,EAAO,MAAM,UAChB,GAAG,EAAO,QAAQ,IAE5B,GAAI,EAAO,WAAa,SAAW,EAAO,WAAa,WAAY,CAC/D,EAAO,OAAO,EAAK,EAAO,WAAa,QAAU,OAAS,OAAO,EACjE,QACJ,CAEA,IAAM,EAAS,MAAM,QAAQ,EAAO,KAAK,EACnC,CAAC,GAAG,EAAO,KAAK,EAChB,CAAC,OAAO,EAAO,OAAS,EAAE,CAAC,EAEjC,GAAI,EAAO,WAAa,UAAW,CAC/B,IAAK,IAAM,KAAS,EAAA,WAAW,CAAM,EAAG,EAAO,OAAO,EAAK,CAAK,EAChE,QACJ,CAEA,IAAK,IAAM,KAAS,EAAQ,EAAO,OAAO,EAAK,CAAK,CACxD,CAEA,OAAO,CACX"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isComplete as e } from "./filter-model.js";
|
|
2
|
+
import { orderRange as t } from "./filter-apply.js";
|
|
3
|
+
//#region src/components/FilterBar/filter-query.ts
|
|
4
|
+
var n = {
|
|
5
|
+
eq: "",
|
|
6
|
+
ne: "__ne",
|
|
7
|
+
contains: "__icontains",
|
|
8
|
+
gt: "__gt",
|
|
9
|
+
gte: "__gte",
|
|
10
|
+
lt: "__lt",
|
|
11
|
+
lte: "__lte",
|
|
12
|
+
between: "__between",
|
|
13
|
+
in: "__in",
|
|
14
|
+
empty: "__isnull",
|
|
15
|
+
notEmpty: "__isnull"
|
|
16
|
+
}, r = "name";
|
|
17
|
+
function i(i) {
|
|
18
|
+
let a = new URLSearchParams();
|
|
19
|
+
for (let o of i) {
|
|
20
|
+
if (!e(o)) continue;
|
|
21
|
+
let i = n[o.operator], s = o.operator === "eq" && o.field === r ? `${o.field}__iexact` : `${o.field}${i}`;
|
|
22
|
+
if (o.operator === "empty" || o.operator === "notEmpty") {
|
|
23
|
+
a.append(s, o.operator === "empty" ? "true" : "false");
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
let c = Array.isArray(o.value) ? [...o.value] : [String(o.value ?? "")];
|
|
27
|
+
if (o.operator === "between") {
|
|
28
|
+
for (let e of t(c)) a.append(s, e);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
for (let e of c) a.append(s, e);
|
|
32
|
+
}
|
|
33
|
+
return a;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { i as filtersToQueryParams };
|
|
37
|
+
|
|
38
|
+
//# sourceMappingURL=filter-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-query.js","names":[],"sources":["../../../src/components/FilterBar/filter-query.ts"],"sourcesContent":["// Server-side half of the filter model: the same `Filter[]` the bar produces,\n// encoded the way a paginated backend expects to read it. Kept apart from\n// `filtersToSearchParams` (filter-model.ts) because the two answer different\n// questions — that one round-trips the UI state through the URL, this one talks\n// to an API and its shape is the API's, not ours.\n\nimport type { Filter, FilterOperator } from \"./filter-model\";\nimport { isComplete } from \"./filter-model\";\nimport { orderRange } from \"./filter-apply\";\n\n/**\n * Operator suffix per filter operator, following the `<column>__<op>` convention\n * of `tempest-fastapi-sdk`.\n *\n * Ported from `build_filter_condition` in\n * `tempest_fastapi_sdk/db/expressions.py`, which is what `BaseRepository`\n * dict filters and `Q` both resolve through. An empty suffix means the bare\n * column name, which that function resolves to plain equality.\n *\n * `contains` maps to `icontains` (case-insensitive, and the backend escapes the\n * `LIKE` wildcards) and both emptiness operators map to `isnull`, whose value\n * carries the direction.\n */\nconst OPERATOR_SUFFIX: Record<FilterOperator, string> = {\n eq: \"\",\n ne: \"__ne\",\n contains: \"__icontains\",\n gt: \"__gt\",\n gte: \"__gte\",\n lt: \"__lt\",\n lte: \"__lte\",\n between: \"__between\",\n in: \"__in\",\n empty: \"__isnull\",\n notEmpty: \"__isnull\",\n};\n\n/**\n * Column name the backend treats as a substring search when it carries no\n * operator suffix.\n *\n * Ported from `build_filter_condition`, which special-cases a string `name` into\n * `ILIKE %value%`. Sending a bare `name=` for an `eq` filter would therefore ask\n * for \"contains\" while the chip says \"is\", so `eq` on this column is emitted as\n * `name__iexact` instead.\n */\nconst SUBSTRING_COLUMN = \"name\";\n\n/**\n * Encode filters as query params for a paginated backend.\n *\n * The counterpart of {@link applyFilters}: same filter set, evaluated by the\n * database instead of by the browser, which is the only option once the list is\n * paginated on the server and the page in memory is not the whole result.\n *\n * The encoding is the `<column>__<op>` convention `tempest-fastapi-sdk` already\n * reads (`BaseRepository._apply_filters` → `build_filter_condition`):\n *\n * | Operator | Param |\n * | --- | --- |\n * | `eq` | `field` (or `field__iexact` for the `name` column) |\n * | `ne` | `field__ne` |\n * | `contains` | `field__icontains` |\n * | `gt` `gte` `lt` `lte` | `field__gt` … `field__lte` |\n * | `between` | `field__between` twice, low value first |\n * | `in` | `field__in` once per value |\n * | `empty` / `notEmpty` | `field__isnull=true` / `=false` |\n *\n * Returns `URLSearchParams` rather than a plain object because `between` carries\n * a pair and `in` carries a list: an object would keep only the last value of\n * each, silently narrowing the filter. Repeated params are also how FastAPI\n * receives a `list[str]` declared with `Query`.\n *\n * Two things the backend has to hold up its end of, or the filter fails quietly:\n *\n * - **Every key must be declared.** `BasePaginationFilterSchema.get_conditions()`\n * only forwards fields the subclass declares, so a `status__ne` the schema\n * never mentions is dropped by FastAPI before the repository sees it — no\n * error, no filtering.\n * - **`isnull` matches `NULL` only.** A column that stores `\"\"` for \"no value\"\n * will not answer an `empty` filter, while {@link applyFilters} treats blank\n * text as empty.\n *\n * @example\n * const params = filtersToQueryParams(filters);\n * params.set(\"page\", String(page));\n * const data = await api.get(`/orders?${params}`);\n *\n * @param filters - Applied filters; incomplete ones are ignored.\n * @returns Params ready to append to a request URL.\n */\nexport function filtersToQueryParams(filters: readonly Filter[]): URLSearchParams {\n const params = new URLSearchParams();\n\n for (const filter of filters) {\n if (!isComplete(filter)) continue;\n\n const suffix = OPERATOR_SUFFIX[filter.operator];\n const key =\n filter.operator === \"eq\" && filter.field === SUBSTRING_COLUMN\n ? `${filter.field}__iexact`\n : `${filter.field}${suffix}`;\n\n if (filter.operator === \"empty\" || filter.operator === \"notEmpty\") {\n params.append(key, filter.operator === \"empty\" ? \"true\" : \"false\");\n continue;\n }\n\n const values = Array.isArray(filter.value)\n ? [...filter.value]\n : [String(filter.value ?? \"\")];\n\n if (filter.operator === \"between\") {\n for (const value of orderRange(values)) params.append(key, value);\n continue;\n }\n\n for (const value of values) params.append(key, value);\n }\n\n return params;\n}\n"],"mappings":";;;AAuBA,IAAM,IAAkD;CACpD,IAAI;CACJ,IAAI;CACJ,UAAU;CACV,IAAI;CACJ,KAAK;CACL,IAAI;CACJ,KAAK;CACL,SAAS;CACT,IAAI;CACJ,OAAO;CACP,UAAU;AACd,GAWM,IAAmB;AA6CzB,SAAgB,EAAqB,GAA6C;CAC9E,IAAM,IAAS,IAAI,gBAAgB;CAEnC,KAAK,IAAM,KAAU,GAAS;EAC1B,IAAI,CAAC,EAAW,CAAM,GAAG;EAEzB,IAAM,IAAS,EAAgB,EAAO,WAChC,IACF,EAAO,aAAa,QAAQ,EAAO,UAAU,IACvC,GAAG,EAAO,MAAM,YAChB,GAAG,EAAO,QAAQ;EAE5B,IAAI,EAAO,aAAa,WAAW,EAAO,aAAa,YAAY;GAC/D,EAAO,OAAO,GAAK,EAAO,aAAa,UAAU,SAAS,OAAO;GACjE;EACJ;EAEA,IAAM,IAAS,MAAM,QAAQ,EAAO,KAAK,IACnC,CAAC,GAAG,EAAO,KAAK,IAChB,CAAC,OAAO,EAAO,SAAS,EAAE,CAAC;EAEjC,IAAI,EAAO,aAAa,WAAW;GAC/B,KAAK,IAAM,KAAS,EAAW,CAAM,GAAG,EAAO,OAAO,GAAK,CAAK;GAChE;EACJ;EAEA,KAAK,IAAM,KAAS,GAAQ,EAAO,OAAO,GAAK,CAAK;CACxD;CAEA,OAAO;AACX"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react");function t(t,n,r={}){let{tickMs:i=1e3}=r,a=(0,e.useCallback)(()=>Math.max(0,t-(Date.now()-n)),[t,n]),[o,s]=(0,e.useState)(a);return(0,e.useEffect)(()=>{let e=a();if(s(e),e<=0)return;let t=setInterval(()=>{let e=a();s(e),e<=0&&clearInterval(t)},i);return()=>clearInterval(t)},[a,i]),o}exports.useCountdown=t;
|
|
2
|
+
//# sourceMappingURL=use-countdown.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-countdown.cjs","names":[],"sources":["../../src/hooks/use-countdown.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** Options for {@link useCountdown}. */\nexport interface UseCountdownOptions {\n /**\n * How often the remaining time is recomputed, in ms. Default `1000`, which\n * suits a \"try again in 3s\" label; drop it to ~50 to drive a progress bar.\n */\n tickMs?: number;\n}\n\n/**\n * Counts down the time left on a window that started at a known instant.\n *\n * Written around a timestamp rather than a decrementing counter on purpose: the\n * remaining time is recomputed from `Date.now()` on every tick, so a throttled\n * background tab, a slow frame or a `setInterval` that drifts cannot make the\n * countdown disagree with the clock. Remounting recovers the correct value too,\n * which a counter held in state cannot do.\n *\n * The interval stops once it reaches zero instead of ticking forever behind a\n * clamp.\n *\n * @param durationMs - Length of the window.\n * @param startedAt - Epoch ms the window opened (`Date.now()` when it started).\n * @param options - Tick cadence.\n * @returns Milliseconds remaining, never below `0`.\n *\n * @example\n * const remaining = useCountdown(60_000, lastSentAt);\n *\n * <button disabled={remaining > 0}>\n * {remaining > 0 ? `Reenviar em ${Math.ceil(remaining / 1000)}s` : \"Reenviar código\"}\n * </button>\n */\nexport function useCountdown(\n durationMs: number,\n startedAt: number,\n options: UseCountdownOptions = {},\n): number {\n const { tickMs = 1000 } = options;\n\n const compute = useCallback(\n () => Math.max(0, durationMs - (Date.now() - startedAt)),\n [durationMs, startedAt],\n );\n\n const [remaining, setRemaining] = useState(compute);\n\n useEffect(() => {\n const initial = compute();\n setRemaining(initial);\n if (initial <= 0) return;\n\n const id = setInterval(() => {\n const next = compute();\n setRemaining(next);\n if (next <= 0) clearInterval(id);\n }, tickMs);\n\n return () => clearInterval(id);\n }, [compute, tickMs]);\n\n return remaining;\n}\n"],"mappings":"uBAmCA,SAAgB,EACZ,EACA,EACA,EAA+B,CAAC,EAC1B,CACN,GAAM,CAAE,SAAS,KAAS,EAEpB,GAAA,EAAU,EAAA,YAAA,KACN,KAAK,IAAI,EAAG,GAAc,KAAK,IAAI,EAAI,EAAU,EACvD,CAAC,EAAY,CAAS,CAC1B,EAEM,CAAC,EAAW,IAAA,EAAgB,EAAA,SAAA,CAAS,CAAO,EAgBlD,OAdA,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EAAU,EAAQ,EAExB,GADA,EAAa,CAAO,EAChB,GAAW,EAAG,OAElB,IAAM,EAAK,gBAAkB,CACzB,IAAM,EAAO,EAAQ,EACrB,EAAa,CAAI,EACb,GAAQ,GAAG,cAAc,CAAE,CACnC,EAAG,CAAM,EAET,UAAa,cAAc,CAAE,CACjC,EAAG,CAAC,EAAS,CAAM,CAAC,EAEb,CACX"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useCallback as e, useEffect as t, useState as n } from "react";
|
|
2
|
+
//#region src/hooks/use-countdown.ts
|
|
3
|
+
function r(r, i, a = {}) {
|
|
4
|
+
let { tickMs: o = 1e3 } = a, s = e(() => Math.max(0, r - (Date.now() - i)), [r, i]), [c, l] = n(s);
|
|
5
|
+
return t(() => {
|
|
6
|
+
let e = s();
|
|
7
|
+
if (l(e), e <= 0) return;
|
|
8
|
+
let t = setInterval(() => {
|
|
9
|
+
let e = s();
|
|
10
|
+
l(e), e <= 0 && clearInterval(t);
|
|
11
|
+
}, o);
|
|
12
|
+
return () => clearInterval(t);
|
|
13
|
+
}, [s, o]), c;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { r as useCountdown };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=use-countdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-countdown.js","names":[],"sources":["../../src/hooks/use-countdown.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** Options for {@link useCountdown}. */\nexport interface UseCountdownOptions {\n /**\n * How often the remaining time is recomputed, in ms. Default `1000`, which\n * suits a \"try again in 3s\" label; drop it to ~50 to drive a progress bar.\n */\n tickMs?: number;\n}\n\n/**\n * Counts down the time left on a window that started at a known instant.\n *\n * Written around a timestamp rather than a decrementing counter on purpose: the\n * remaining time is recomputed from `Date.now()` on every tick, so a throttled\n * background tab, a slow frame or a `setInterval` that drifts cannot make the\n * countdown disagree with the clock. Remounting recovers the correct value too,\n * which a counter held in state cannot do.\n *\n * The interval stops once it reaches zero instead of ticking forever behind a\n * clamp.\n *\n * @param durationMs - Length of the window.\n * @param startedAt - Epoch ms the window opened (`Date.now()` when it started).\n * @param options - Tick cadence.\n * @returns Milliseconds remaining, never below `0`.\n *\n * @example\n * const remaining = useCountdown(60_000, lastSentAt);\n *\n * <button disabled={remaining > 0}>\n * {remaining > 0 ? `Reenviar em ${Math.ceil(remaining / 1000)}s` : \"Reenviar código\"}\n * </button>\n */\nexport function useCountdown(\n durationMs: number,\n startedAt: number,\n options: UseCountdownOptions = {},\n): number {\n const { tickMs = 1000 } = options;\n\n const compute = useCallback(\n () => Math.max(0, durationMs - (Date.now() - startedAt)),\n [durationMs, startedAt],\n );\n\n const [remaining, setRemaining] = useState(compute);\n\n useEffect(() => {\n const initial = compute();\n setRemaining(initial);\n if (initial <= 0) return;\n\n const id = setInterval(() => {\n const next = compute();\n setRemaining(next);\n if (next <= 0) clearInterval(id);\n }, tickMs);\n\n return () => clearInterval(id);\n }, [compute, tickMs]);\n\n return remaining;\n}\n"],"mappings":";;AAmCA,SAAgB,EACZ,GACA,GACA,IAA+B,CAAC,GAC1B;CACN,IAAM,EAAE,YAAS,QAAS,GAEpB,IAAU,QACN,KAAK,IAAI,GAAG,KAAc,KAAK,IAAI,IAAI,EAAU,GACvD,CAAC,GAAY,CAAS,CAC1B,GAEM,CAAC,GAAW,KAAgB,EAAS,CAAO;CAgBlD,OAdA,QAAgB;EACZ,IAAM,IAAU,EAAQ;EAExB,IADA,EAAa,CAAO,GAChB,KAAW,GAAG;EAElB,IAAM,IAAK,kBAAkB;GACzB,IAAM,IAAO,EAAQ;GAErB,AADA,EAAa,CAAI,GACb,KAAQ,KAAG,cAAc,CAAE;EACnC,GAAG,CAAM;EAET,aAAa,cAAc,CAAE;CACjC,GAAG,CAAC,GAAS,CAAM,CAAC,GAEb;AACX"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-latest-ref.cjs","names":[],"sources":["../../src/hooks/use-latest-ref.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- writing at render time is deliberate; an effect would add a staleness window (see the docstring) */\nimport { useRef, type RefObject } from \"react\";\n\n/**\n * Keeps a ref pointing at the most recent value it was given.\n *\n * The escape hatch for reading fresh state inside something that must not be\n * re-created when that state changes: an interval, a subscription, an event\n * listener registered once on mount. Listing the value in the effect's\n * dependencies would tear the effect down and set it back up on every change;\n * omitting it captures the value from the render that created the closure and\n * never sees another. The ref is neither — a stable object whose `current` is\n * always current.\n *\n * The assignment happens **during render**, for the same reason\n * {@link useStableCallback} does it: moving it into an effect opens a\n * one-commit staleness window, where an effect declared earlier in the same\n * commit reads the previous render's value.\n *\n * Reach for {@link useStableCallback} instead when the value is a function you\n * want to *call* — it hands back a callable with a stable identity, rather than\n * making every call site reach through `.current`.\n *\n * @typeParam T - The tracked value.\n * @param value - The value to track. Written on every render.\n * @returns A stable ref whose `current` holds the latest `value`.\n *\n * @example\n * const optionsRef = useLatestRef(options);\n *\n * useEffect(() => {\n * const id = setInterval(() => poll(optionsRef.current), 5_000);\n * return () => clearInterval(id);\n * }, []); // the interval survives every options change, and still reads the latest\n */\nexport function useLatestRef<T>(value: T): RefObject<T> {\n const ref = useRef(value);\n ref.current = value;\n return ref;\n}\n"],"mappings":"uBAmCA,SAAgB,EAAgB,EAAwB,CACpD,IAAM,GAAA,EAAM,EAAA,OAAA,CAAO,CAAK,EAExB,MADA,GAAI,QAAU,EACP,CACX"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-latest-ref.js","names":[],"sources":["../../src/hooks/use-latest-ref.ts"],"sourcesContent":["/* eslint-disable react-hooks/refs -- writing at render time is deliberate; an effect would add a staleness window (see the docstring) */\nimport { useRef, type RefObject } from \"react\";\n\n/**\n * Keeps a ref pointing at the most recent value it was given.\n *\n * The escape hatch for reading fresh state inside something that must not be\n * re-created when that state changes: an interval, a subscription, an event\n * listener registered once on mount. Listing the value in the effect's\n * dependencies would tear the effect down and set it back up on every change;\n * omitting it captures the value from the render that created the closure and\n * never sees another. The ref is neither — a stable object whose `current` is\n * always current.\n *\n * The assignment happens **during render**, for the same reason\n * {@link useStableCallback} does it: moving it into an effect opens a\n * one-commit staleness window, where an effect declared earlier in the same\n * commit reads the previous render's value.\n *\n * Reach for {@link useStableCallback} instead when the value is a function you\n * want to *call* — it hands back a callable with a stable identity, rather than\n * making every call site reach through `.current`.\n *\n * @typeParam T - The tracked value.\n * @param value - The value to track. Written on every render.\n * @returns A stable ref whose `current` holds the latest `value`.\n *\n * @example\n * const optionsRef = useLatestRef(options);\n *\n * useEffect(() => {\n * const id = setInterval(() => poll(optionsRef.current), 5_000);\n * return () => clearInterval(id);\n * }, []); // the interval survives every options change, and still reads the latest\n */\nexport function useLatestRef<T>(value: T): RefObject<T> {\n const ref = useRef(value);\n ref.current = value;\n return ref;\n}\n"],"mappings":";;AAmCA,SAAgB,EAAgB,GAAwB;CACpD,IAAM,IAAM,EAAO,CAAK;CAExB,OADA,EAAI,UAAU,GACP;AACX"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react");function t(t,n){let r=t??``,i=n>0?0:r.length,[a,o]=(0,e.useState)(r),[s,c]=(0,e.useState)(i);a!==r&&(o(r),c(i)),(0,e.useEffect)(()=>{if(n<=0||r.length===0){c(r.length);return}let e=setInterval(()=>{c(t=>{let n=t+1;return n>=r.length?(clearInterval(e),r.length):n})},n);return()=>clearInterval(e)},[r,n]);let l=(0,e.useCallback)(()=>c(r.length),[r.length]);return{displayedText:r.slice(0,s),isComplete:s>=r.length,skip:l}}exports.useTypewriter=t;
|
|
2
|
+
//# sourceMappingURL=use-typewriter.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-typewriter.cjs","names":[],"sources":["../../src/hooks/use-typewriter.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** State returned by {@link useTypewriter}. */\nexport interface UseTypewriterResult {\n /** The prefix of `text` revealed so far. */\n displayedText: string;\n /** `true` once the whole string is on screen. */\n isComplete: boolean;\n /** Reveal the rest immediately — wire it to a tap or a key press. */\n skip: () => void;\n}\n\n/**\n * Reveals a string one character at a time.\n *\n * Changing `text` restarts the reveal. The reset happens during render rather\n * than in an effect, so the new string never flashes in full for one frame\n * before the animation takes over.\n *\n * Always give the reader a way out: an animation that cannot be skipped is a\n * tax on anyone re-reading or moving fast, which is what `skip` is for.\n *\n * @param text - The full string to reveal. Nullish is treated as empty.\n * @param speedMs - Delay between characters. `0` or less renders instantly,\n * which is the hook-safe way to honour `prefers-reduced-motion`.\n * @returns The revealed prefix, whether it finished, and a `skip` action.\n *\n * @example\n * const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n * const { displayedText, isComplete, skip } = useTypewriter(line, reduced ? 0 : 30);\n *\n * return <p onClick={skip}>{displayedText}{isComplete ? \"\" : \"▌\"}</p>;\n */\nexport function useTypewriter(text: string, speedMs: number): UseTypewriterResult {\n const safeText = text ?? \"\";\n const initialCount = speedMs > 0 ? 0 : safeText.length;\n\n const [renderedText, setRenderedText] = useState(safeText);\n const [count, setCount] = useState(initialCount);\n\n if (renderedText !== safeText) {\n setRenderedText(safeText);\n setCount(initialCount);\n }\n\n useEffect(() => {\n if (speedMs <= 0 || safeText.length === 0) {\n setCount(safeText.length);\n return;\n }\n\n const id = setInterval(() => {\n setCount((current) => {\n const next = current + 1;\n if (next >= safeText.length) {\n clearInterval(id);\n return safeText.length;\n }\n return next;\n });\n }, speedMs);\n\n return () => clearInterval(id);\n }, [safeText, speedMs]);\n\n const skip = useCallback(() => setCount(safeText.length), [safeText.length]);\n\n return {\n displayedText: safeText.slice(0, count),\n isComplete: count >= safeText.length,\n skip,\n };\n}\n"],"mappings":"uBAiCA,SAAgB,EAAc,EAAc,EAAsC,CAC9E,IAAM,EAAW,GAAQ,GACnB,EAAe,EAAU,EAAI,EAAI,EAAS,OAE1C,CAAC,EAAc,IAAA,EAAmB,EAAA,SAAA,CAAS,CAAQ,EACnD,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAS,CAAY,EAE3C,IAAiB,IACjB,EAAgB,CAAQ,EACxB,EAAS,CAAY,IAGzB,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,GAAW,GAAK,EAAS,SAAW,EAAG,CACvC,EAAS,EAAS,MAAM,EACxB,MACJ,CAEA,IAAM,EAAK,gBAAkB,CACzB,EAAU,GAAY,CAClB,IAAM,EAAO,EAAU,EAKvB,OAJI,GAAQ,EAAS,QACjB,cAAc,CAAE,EACT,EAAS,QAEb,CACX,CAAC,CACL,EAAG,CAAO,EAEV,UAAa,cAAc,CAAE,CACjC,EAAG,CAAC,EAAU,CAAO,CAAC,EAEtB,IAAM,GAAA,EAAO,EAAA,YAAA,KAAkB,EAAS,EAAS,MAAM,EAAG,CAAC,EAAS,MAAM,CAAC,EAE3E,MAAO,CACH,cAAe,EAAS,MAAM,EAAG,CAAK,EACtC,WAAY,GAAS,EAAS,OAC9B,MACJ,CACJ"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback as e, useEffect as t, useState as n } from "react";
|
|
2
|
+
//#region src/hooks/use-typewriter.ts
|
|
3
|
+
function r(r, i) {
|
|
4
|
+
let a = r ?? "", o = i > 0 ? 0 : a.length, [s, c] = n(a), [l, u] = n(o);
|
|
5
|
+
s !== a && (c(a), u(o)), t(() => {
|
|
6
|
+
if (i <= 0 || a.length === 0) {
|
|
7
|
+
u(a.length);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
let e = setInterval(() => {
|
|
11
|
+
u((t) => {
|
|
12
|
+
let n = t + 1;
|
|
13
|
+
return n >= a.length ? (clearInterval(e), a.length) : n;
|
|
14
|
+
});
|
|
15
|
+
}, i);
|
|
16
|
+
return () => clearInterval(e);
|
|
17
|
+
}, [a, i]);
|
|
18
|
+
let d = e(() => u(a.length), [a.length]);
|
|
19
|
+
return {
|
|
20
|
+
displayedText: a.slice(0, l),
|
|
21
|
+
isComplete: l >= a.length,
|
|
22
|
+
skip: d
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { r as useTypewriter };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=use-typewriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-typewriter.js","names":[],"sources":["../../src/hooks/use-typewriter.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\n/** State returned by {@link useTypewriter}. */\nexport interface UseTypewriterResult {\n /** The prefix of `text` revealed so far. */\n displayedText: string;\n /** `true` once the whole string is on screen. */\n isComplete: boolean;\n /** Reveal the rest immediately — wire it to a tap or a key press. */\n skip: () => void;\n}\n\n/**\n * Reveals a string one character at a time.\n *\n * Changing `text` restarts the reveal. The reset happens during render rather\n * than in an effect, so the new string never flashes in full for one frame\n * before the animation takes over.\n *\n * Always give the reader a way out: an animation that cannot be skipped is a\n * tax on anyone re-reading or moving fast, which is what `skip` is for.\n *\n * @param text - The full string to reveal. Nullish is treated as empty.\n * @param speedMs - Delay between characters. `0` or less renders instantly,\n * which is the hook-safe way to honour `prefers-reduced-motion`.\n * @returns The revealed prefix, whether it finished, and a `skip` action.\n *\n * @example\n * const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n * const { displayedText, isComplete, skip } = useTypewriter(line, reduced ? 0 : 30);\n *\n * return <p onClick={skip}>{displayedText}{isComplete ? \"\" : \"▌\"}</p>;\n */\nexport function useTypewriter(text: string, speedMs: number): UseTypewriterResult {\n const safeText = text ?? \"\";\n const initialCount = speedMs > 0 ? 0 : safeText.length;\n\n const [renderedText, setRenderedText] = useState(safeText);\n const [count, setCount] = useState(initialCount);\n\n if (renderedText !== safeText) {\n setRenderedText(safeText);\n setCount(initialCount);\n }\n\n useEffect(() => {\n if (speedMs <= 0 || safeText.length === 0) {\n setCount(safeText.length);\n return;\n }\n\n const id = setInterval(() => {\n setCount((current) => {\n const next = current + 1;\n if (next >= safeText.length) {\n clearInterval(id);\n return safeText.length;\n }\n return next;\n });\n }, speedMs);\n\n return () => clearInterval(id);\n }, [safeText, speedMs]);\n\n const skip = useCallback(() => setCount(safeText.length), [safeText.length]);\n\n return {\n displayedText: safeText.slice(0, count),\n isComplete: count >= safeText.length,\n skip,\n };\n}\n"],"mappings":";;AAiCA,SAAgB,EAAc,GAAc,GAAsC;CAC9E,IAAM,IAAW,KAAQ,IACnB,IAAe,IAAU,IAAI,IAAI,EAAS,QAE1C,CAAC,GAAc,KAAmB,EAAS,CAAQ,GACnD,CAAC,GAAO,KAAY,EAAS,CAAY;CAO/C,AALI,MAAiB,MACjB,EAAgB,CAAQ,GACxB,EAAS,CAAY,IAGzB,QAAgB;EACZ,IAAI,KAAW,KAAK,EAAS,WAAW,GAAG;GACvC,EAAS,EAAS,MAAM;GACxB;EACJ;EAEA,IAAM,IAAK,kBAAkB;GACzB,GAAU,MAAY;IAClB,IAAM,IAAO,IAAU;IAKvB,OAJI,KAAQ,EAAS,UACjB,cAAc,CAAE,GACT,EAAS,UAEb;GACX,CAAC;EACL,GAAG,CAAO;EAEV,aAAa,cAAc,CAAE;CACjC,GAAG,CAAC,GAAU,CAAO,CAAC;CAEtB,IAAM,IAAO,QAAkB,EAAS,EAAS,MAAM,GAAG,CAAC,EAAS,MAAM,CAAC;CAE3E,OAAO;EACH,eAAe,EAAS,MAAM,GAAG,CAAK;EACtC,YAAY,KAAS,EAAS;EAC9B;CACJ;AACJ"}
|
package/dist/http/api-client.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../utils/ids.cjs"),t=require("./
|
|
1
|
+
const e=require("../utils/ids.cjs"),t=require("./build-url.cjs"),n=require("./errors.cjs"),r=require("./retry.cjs");var i=new Set([`GET`,`HEAD`,`OPTIONS`]),a=new Set([0,408,425,429]);function o(e,t){return!i.has(t)||!(e instanceof n.TempestApiError)?!1:a.has(e.status)||e.status>=500}function s(e){return e?e===!0?{}:e:null}function c(e){return typeof FormData<`u`&&e instanceof FormData}async function l(e,t){let r;try{r=await e.clone().json()}catch{try{r=await e.text()}catch{r=null}}return new n.TempestApiError(n.buildApiError(e.status,r,e.headers,t))}function u(n){let i=n.fetcher??globalThis.fetch.bind(globalThis);function a(){let e=n.getToken?.();return e?{Authorization:`Bearer ${e}`}:{}}async function u(e,r,o){let{body:s,params:l,headers:u,...d}=r,f=c(s),p={...f?{}:{"Content-Type":`application/json`},...o?{"X-Request-ID":o}:{},...n.headers,...a(),...u},m={...d,headers:p,credentials:n.withCredentials?`include`:d.credentials,body:s==null?void 0:f?s:JSON.stringify(s)};return i(t.buildApiUrl(n.baseURL,e,{prefix:n.prefix,params:l}),m)}async function d(t,r){let i=n.requestId?n.requestId():e.randomId(),a=await u(t,r,i);if(a.status===401){if(n.refresh){try{await n.refresh(),a=await u(t,r,i)}catch{throw await n.onUnauthorized?.(a),await l(a,i)}a.status===401&&await n.onUnauthorized?.(a)}else await n.onUnauthorized?.(a)}if(!a.ok)throw await l(a,i);if(a.status!==204)return(a.headers.get(`content-type`)??``).includes(`application/json`)?await a.json():await a.text()}async function f(e,t={}){let i=s(n.retry);if(!i)return d(e,t);let a=(t.method??`GET`).toUpperCase();return r.retry(()=>d(e,t),{...i,shouldRetry:i.shouldRetry??(e=>o(e,a))})}async function p(e,t,n=`POST`){return f(e,{method:n,body:t})}return{request:f,get:(e,t)=>f(e,{...t,method:`GET`}),post:(e,t)=>f(e,{...t,method:`POST`}),put:(e,t)=>f(e,{...t,method:`PUT`}),patch:(e,t)=>f(e,{...t,method:`PATCH`}),delete:(e,t)=>f(e,{...t,method:`DELETE`}),upload:p}}exports.createApiClient=u;
|
|
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 { buildApiError, TempestApiError } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\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 */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\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 RETRIABLE_STATUSES.has(error.status) || error.status >= 500;\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\nfunction buildUrl(baseURL: string, path: string, params?: RequestOptions[\"params\"]): string {\n const url = new URL(path, baseURL.endsWith(\"/\") ? baseURL : `${baseURL}/`);\n if (params) {\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value));\n }\n }\n }\n return url.toString();\n}\n\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\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 * @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 * 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, ...rest } = options;\n const isForm = isFormData(body);\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 init: RequestInit = {\n ...rest,\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 return fetcher(buildUrl(config.baseURL, path, params), init);\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n let response = await rawRequest(path, options, requestId);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await rawRequest(path, options, requestId);\n } catch {\n await config.onUnauthorized?.(response);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await config.onUnauthorized?.(response);\n }\n } else {\n await config.onUnauthorized?.(response);\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 ): Promise<T> {\n return request<T>(path, { 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 { buildApiError, TempestApiError } from \"./errors\";\nimport { retry as retryWithBackoff } from \"./retry\";\nimport type { RetryOptions } from \"./retry\";\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 */\nconst RETRIABLE_STATUSES: ReadonlySet<number> = new Set([0, 408, 425, 429]);\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 RETRIABLE_STATUSES.has(error.status) || error.status >= 500;\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\nfunction isFormData(body: unknown): body is FormData {\n return typeof FormData !== \"undefined\" && body instanceof FormData;\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 * @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 * 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, ...rest } = options;\n const isForm = isFormData(body);\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 init: RequestInit = {\n ...rest,\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 return fetcher(buildApiUrl(config.baseURL, path, { prefix: config.prefix, params }), init);\n }\n\n async function attempt<T>(path: string, options: RequestOptions): Promise<T> {\n const requestId = config.requestId ? config.requestId() : randomId();\n let response = await rawRequest(path, options, requestId);\n\n if (response.status === 401) {\n if (config.refresh) {\n try {\n await config.refresh();\n response = await rawRequest(path, options, requestId);\n } catch {\n await config.onUnauthorized?.(response);\n throw await parseError(response, requestId);\n }\n if (response.status === 401) {\n await config.onUnauthorized?.(response);\n }\n } else {\n await config.onUnauthorized?.(response);\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 ): Promise<T> {\n return request<T>(path, { 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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"}
|