tempest-react-sdk 0.42.1 → 0.44.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 +13 -7
- package/bin/create-tempest-app.mjs +10 -1
- 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/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/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/router/AppRouter.cjs.map +1 -1
- package/dist/router/AppRouter.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/tempest-react-sdk.cjs +1 -1
- package/dist/tempest-react-sdk.d.ts +790 -10
- package/dist/tempest-react-sdk.js +275 -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 +3 -2
- package/template/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,28 +159,30 @@ Requires React `>=18` and Node `>=22.12` to build.
|
|
|
159
159
|
|
|
160
160
|
### Peer & bundled dependencies
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
**react**, **react-dom** and **react-router** are peer dependencies — those must come from the host app, because a second copy is not just wasted bytes, it is a second _instance_ of a React context and it breaks at runtime.
|
|
163
163
|
|
|
164
|
-
Everything else (`zod`, `zustand`, `dexie`, `react-hook-form`, `@tanstack/react-query`, `
|
|
164
|
+
Everything else (`zod`, `zustand`, `dexie`, `react-hook-form`, `@tanstack/react-query`, `lucide-react`) is a **direct dependency** of the SDK, installed automatically by `npm install tempest-react-sdk`. You never need to install them manually.
|
|
165
165
|
|
|
166
166
|
| Package | Status | Used by |
|
|
167
167
|
| ------------------------------------- | ------------------- | ----------------------------------------------------------------------- |
|
|
168
168
|
| `react`, `react-dom` (`^18 \|\| ^19`) | **Peer (required)** | Everything |
|
|
169
|
+
| `react-router` (`^7 \|\| ^8`) | **Peer (required)** | `AppRouter`, `defineRoutes`, `RouteGuard`, routing re-exports |
|
|
169
170
|
| `@tanstack/react-query` (`^5`) | Direct dep (auto) | `QueryProvider`, `createQueryKeys`, `AppProviders` |
|
|
170
171
|
| `zod` (`^3.23 \|\| ^4`) | Direct dep (auto) | `parseResponse`, `validateForm`, `zodResolver`, `useZodForm` |
|
|
171
172
|
| `zustand` (`^4 \|\| ^5`) | Direct dep (auto) | `createAuthStore`, `createStore`, `createSelectors` |
|
|
172
|
-
| `react-router` (`^7`) | Direct dep (auto) | `AppRouter`, `defineRoutes`, `RouteGuard`, routing re-exports |
|
|
173
173
|
| `dexie` (`^4.4`) | Direct dep (auto) | `createOfflineStore` |
|
|
174
174
|
| `react-hook-form` (`^7.76`) | Direct dep (auto) | `zodResolver`, `useZodForm`, masked inputs |
|
|
175
|
-
| `lucide-react` (
|
|
175
|
+
| `lucide-react` (`^1.31`) | Direct dep (auto) | Component icons (`leftIcon`/`rightIcon` on `Input`, `Button`, etc.) |
|
|
176
176
|
| `vite`, `@vitejs/plugin-react` | **Optional peer** | `createViteConfig` (`tempest-react-sdk/vite`) — already in any Vite app |
|
|
177
177
|
|
|
178
178
|
The minimum install is just:
|
|
179
179
|
|
|
180
180
|
```bash
|
|
181
|
-
npm install tempest-react-sdk react react-dom
|
|
181
|
+
npm install tempest-react-sdk react react-dom react-router
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
**Why `react-router` is a peer and not a bundled dep.** It holds React context. A copy nested under `tempest-react-sdk/node_modules` is a _different_ `<Router>` context than the one your app renders, so any SDK hook reaching for it throws `useNavigate() may be used only in the context of a <Router>` — a runtime crash, not a size regression. That is the same reason `react` itself is a peer, and it is why `react-router` is the one exception to the "everything is a direct dep" rule. The `^7 || ^8` range means an app on either major installs one copy and the SDK adapts to it: the re-exported surface is identical across both, and both ship the DOM bindings inside `react-router` (there is no separate `react-router-dom`).
|
|
185
|
+
|
|
184
186
|
**Bundle impact**: every bundled dep is externalised in the SDK's Rollup config, and `dist/` ships with the module graph preserved (one file per source module), so your bundler drops what you don't import instead of inheriting one opaque blob — importing `cn` alone costs **153 B** brotli, a full app shell around **6.8 KB**. Your app's bundler (Vite / webpack / Rspack) resolves the deps from `node_modules` and tree-shakes them the same way — if you never call `createOfflineStore`, Dexie never enters your final bundle.
|
|
185
187
|
|
|
186
188
|
**Version conflicts**: if your app already pins (say) `zod@3.20`, npm dedupes when the range is compatible. If ranges diverge you get two copies — pin a single version in your own `package.json` to force one, or open an issue if the SDK's range is too tight.
|
|
@@ -256,7 +258,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
256
258
|
| `perf` | `createInferenceProfiler`, `readDeviceProfile`, `cachedResponseBytes`, `formatDurationMs`, types: `InferenceProfiler`, `InferenceReport`, `InferenceReportOptions`, `DeviceProfile`, `ProfiledModel`, `ProfiledModelSize` — custo de inferência on-device: cronômetro por etapa, perfil do dispositivo (cores/RAM/heap) e tamanho dos modelos em Cache Storage |
|
|
257
259
|
| `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
|
|
258
260
|
| `share` | `share`, `isShareSupported`, `shareOrDownloadBlob` (Web Share file → download fallback), types: `SharePayload`, `ShareResult`, `ShareOrDownloadOptions` |
|
|
259
|
-
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`)
|
|
261
|
+
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateForInput`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`, `percentOf`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`), CSV (`toCsv`, `downloadCsv` — RFC 4180 escaping + BOM) |
|
|
260
262
|
| generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
|
|
261
263
|
| Module | Exports |
|
|
262
264
|
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -293,7 +295,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
|
|
|
293
295
|
| `perf` | `createInferenceProfiler`, `readDeviceProfile`, `cachedResponseBytes`, `formatDurationMs`, types: `InferenceProfiler`, `InferenceReport`, `InferenceReportOptions`, `DeviceProfile`, `ProfiledModel`, `ProfiledModelSize` — custo de inferência on-device: cronômetro por etapa, perfil do dispositivo (cores/RAM/heap) e tamanho dos modelos em Cache Storage |
|
|
294
296
|
| `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
|
|
295
297
|
| `share` | `share`, `isShareSupported`, `shareOrDownloadBlob` (Web Share file → download fallback), types: `SharePayload`, `ShareResult`, `ShareOrDownloadOptions` |
|
|
296
|
-
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`)
|
|
298
|
+
| `utils` | `cn`, format BR (`formatCurrency`, `formatDate`, `formatDateForInput`, `formatDateTime`, `formatPhone`, `formatCPF`, `formatPercent`), `storage`, strings (`slugify`, `truncate`, `capitalize`, `camelCase`, `kebabCase`, `pluralize`), numbers (`clamp`, `formatBytes`, `formatCompactNumber`, `percentOf`), arrays (`groupBy`, `uniqueBy`, `chunk`, `range`), objects (`pick`, `omit`, `deepMerge`, `isEmpty`), guards (`isDefined`, `isString`, `isNumber`, `isPlainObject`, `assertNever`), functions (`debounce`, `throttle`, `once`, `memoizeOne`), promises (`sleep`, `withTimeout`), `randomId`, `relativeTime`, `writeXlsx` (generic single-sheet OOXML `.xlsx` writer → `Uint8Array`), CSV (`toCsv`, `downloadCsv` — RFC 4180 escaping + BOM) |
|
|
297
299
|
| generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
|
|
298
300
|
|
|
299
301
|
Full per-module docs are published as a bilingual MkDocs site on GitHub Pages — **[Português (BR)](https://mauriciobenjamin700.github.io/tempest-react-sdk/)** / **[English (US)](https://mauriciobenjamin700.github.io/tempest-react-sdk/en/)** (one page per module + draw.io diagrams in [`docs/diagrams/`](./docs/diagrams)). The source markdown lives in [`docs/`](./docs) (PT-BR base files + `.en.md` translations).
|
|
@@ -1749,20 +1751,24 @@ import {
|
|
|
1749
1751
|
cn,
|
|
1750
1752
|
formatCurrency,
|
|
1751
1753
|
formatDate,
|
|
1754
|
+
formatDateForInput,
|
|
1752
1755
|
formatDateTime,
|
|
1753
1756
|
formatPhone,
|
|
1754
1757
|
formatCPF,
|
|
1755
1758
|
formatPercent,
|
|
1759
|
+
percentOf,
|
|
1756
1760
|
storage,
|
|
1757
1761
|
} from "tempest-react-sdk";
|
|
1758
1762
|
|
|
1759
1763
|
cn("btn", isPrimary && "btn-primary", { "btn-disabled": disabled });
|
|
1760
1764
|
formatCurrency(1234.5, "BRL"); // "R$ 1.234,50"
|
|
1761
1765
|
formatDate("2026-05-17"); // "17/05/2026"
|
|
1766
|
+
formatDateForInput("2026-05-17T14:30Z"); // "2026-05-17" — the value <input type="date"> accepts
|
|
1762
1767
|
formatDateTime("2026-05-17T14:30Z"); // "17/05/2026 11:30"
|
|
1763
1768
|
formatPhone("11998765432"); // "(11) 99876-5432"
|
|
1764
1769
|
formatCPF("12345678909"); // "123.456.789-09"
|
|
1765
1770
|
formatPercent(0.1234, 1); // "12,3%"
|
|
1771
|
+
percentOf(3, 4); // 75 — and percentOf(3, 0) is 0, not NaN
|
|
1766
1772
|
|
|
1767
1773
|
storage.set("draft", { title: "..." });
|
|
1768
1774
|
const draft = storage.get<Draft>("draft");
|
|
@@ -67,7 +67,16 @@ async function isEmptyDir(dir) {
|
|
|
67
67
|
return entries.filter((e) => e !== ".git").length === 0;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Read the SDK's own version so the generated app pins a matching range.
|
|
72
|
+
*
|
|
73
|
+
* This is why `template/package.json` deliberately does **not** list
|
|
74
|
+
* `tempest-react-sdk` in its dependencies: every write path below stamps the
|
|
75
|
+
* live version over whatever is there, so a literal range in the template is a
|
|
76
|
+
* dead value that only drifts (it sat at `^0.27.0` fifteen minors after the
|
|
77
|
+
* fact) and misleads whoever reads the file. `test/scaffold-template.test.ts`
|
|
78
|
+
* keeps it out.
|
|
79
|
+
*/
|
|
71
80
|
async function readSdkVersion() {
|
|
72
81
|
try {
|
|
73
82
|
const pkg = JSON.parse(await readFile(join(PKG_ROOT, "package.json"), "utf8"));
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../
|
|
1
|
+
const e=require("../i18n/I18nProvider.cjs"),t=require("../query/QueryProvider.cjs"),n=require("../theme/ThemeProvider.cjs"),r=require("../error-boundary/ErrorBoundary.cjs");let i=require("react/jsx-runtime");function a({children:a,query:o,theme:s,i18n:c,errorBoundary:l}){let u=a;return c&&(u=(0,i.jsx)(e.I18nProvider,{...c,children:u})),s!==!1&&(u=(0,i.jsx)(n.ThemeProvider,{...s||{},children:u})),o!==!1&&(u=(0,i.jsx)(t.QueryProvider,{...o||{},children:u})),l&&(u=(0,i.jsx)(r.ErrorBoundary,{...l,children:u})),(0,i.jsx)(i.Fragment,{children:u})}exports.AppProviders=a;
|
|
2
2
|
//# sourceMappingURL=AppProviders.cjs.map
|
package/dist/app/AppProviders.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { I18nProvider as e } from "../i18n/I18nProvider.js";
|
|
2
|
+
import { QueryProvider as t } from "../query/QueryProvider.js";
|
|
3
|
+
import { ThemeProvider as n } from "../theme/ThemeProvider.js";
|
|
4
4
|
import { ErrorBoundary as r } from "../error-boundary/ErrorBoundary.js";
|
|
5
5
|
import { Fragment as i, jsx as a } from "react/jsx-runtime";
|
|
6
6
|
//#region src/app/AppProviders.tsx
|
|
7
7
|
function o({ children: o, query: s, theme: c, i18n: l, errorBoundary: u }) {
|
|
8
8
|
let d = o;
|
|
9
|
-
return l && (d = /* @__PURE__ */ a(
|
|
9
|
+
return l && (d = /* @__PURE__ */ a(e, {
|
|
10
10
|
...l,
|
|
11
11
|
children: d
|
|
12
|
-
})), c !== !1 && (d = /* @__PURE__ */ a(
|
|
12
|
+
})), c !== !1 && (d = /* @__PURE__ */ a(n, {
|
|
13
13
|
...c || {},
|
|
14
14
|
children: d
|
|
15
|
-
})), s !== !1 && (d = /* @__PURE__ */ a(
|
|
15
|
+
})), s !== !1 && (d = /* @__PURE__ */ a(t, {
|
|
16
16
|
...s || {},
|
|
17
17
|
children: d
|
|
18
18
|
})), u && (d = /* @__PURE__ */ a(r, {
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(e){return Number.isFinite(e)?Math.min(1,Math.max(0,e)):1}function t(t={}){let{baseUrl:n=``,voices:r=1,maxSources:i=48}=t,a=e(t.volume??1),o=Math.max(1,Math.floor(r)),s=new Map,c=new WeakMap;function l(e){return!n||/^(https?:)?\/\//.test(e)||e.startsWith(`data:`)?e:`${n.replace(/\/$/,``)}/${e.replace(/^\//,``)}`}function u(e){for(let t of e.elements)t.pause(),t.removeAttribute(`src`),t.load()}function d(e){if(typeof Audio>`u`)return null;let t=l(e),n=s.get(t);if(n)return s.delete(t),s.set(t,n),n;let r={elements:Array.from({length:o},()=>{let e=new Audio(t);return e.preload=`auto`,e}),next:0};if(s.set(t,r),s.size>i){let e=s.keys().next();if(!e.done&&e.value!==t){let t=s.get(e.value);t&&u(t),s.delete(e.value)}}return r}return{play(t,n={}){let r=d(t);if(!r)return;let i=r.elements[r.next];r.next=(r.next+1)%r.elements.length;let o=e(n.volume??1);c.set(i,o),i.pause(),i.currentTime=0,i.volume=e(o*a),i.play().catch(()=>{})},preload(e){for(let t of Array.isArray(e)?e:[e])d(t)?.elements.forEach(e=>e.load())},setVolume(t){a=e(t);for(let t of s.values())for(let n of t.elements)n.paused||(n.volume=e((c.get(n)??1)*a))},stop(e){let t=e?[s.get(l(e))]:[...s.values()];for(let e of t)if(e)for(let t of e.elements)t.pause(),t.currentTime=0},dispose(){for(let e of s.values())u(e);s.clear()}}}exports.createSfxPool=t;
|
|
2
|
+
//# sourceMappingURL=sfx-pool.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sfx-pool.cjs","names":[],"sources":["../../src/audio/sfx-pool.ts"],"sourcesContent":["/** Options for {@link createSfxPool}. */\nexport interface SfxPoolOptions {\n /**\n * Master volume, `0`–`1`, multiplied into every per-play volume. Default\n * `1`. Wire it to the app's sound setting so one value governs the lot.\n */\n volume?: number;\n /**\n * Prefix applied to sources that are not absolute URLs — typically Vite's\n * `import.meta.env.BASE_URL`, so a build served from a subpath resolves.\n * Default `\"\"`.\n */\n baseUrl?: string;\n /**\n * Elements kept per source. `1` (the default) restarts the clip on every\n * play, which is what a menu blip wants. Raise it to let a sound overlap\n * itself — a hit landing while the previous one is still ringing.\n */\n voices?: number;\n /**\n * Maximum number of distinct sources held. When exceeded, the least\n * recently played source is released. Default `48`.\n */\n maxSources?: number;\n}\n\n/** Per-play overrides. */\nexport interface PlaySfxOptions {\n /** Volume for this play, `0`–`1`, multiplied by the pool's master. Default `1`. */\n volume?: number;\n}\n\n/** Imperative handle over a pool of short sound effects. */\nexport interface SfxPool {\n /** Play a clip, allocating and caching its element on first use. */\n play: (src: string, options?: PlaySfxOptions) => void;\n /** Fetch clips ahead of the first play, so it is not silent while the file downloads. */\n preload: (src: string | string[]) => void;\n /** Set the master volume, applying it to anything already sounding. */\n setVolume: (volume: number) => void;\n /** Stop one source, or every source when called with no argument. */\n stop: (src?: string) => void;\n /** Release every element. Call on unmount. */\n dispose: () => void;\n}\n\n/** Clamp to the range `HTMLMediaElement.volume` accepts, rejecting `NaN`. */\nfunction clampVolume(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * A pool of preallocated `<audio>` elements for short sound effects.\n *\n * `new Audio(src)` on every play allocates an element and re-enters the network\n * stack for a file the browser already has, which is the wrong shape for a\n * sound that fires dozens of times a minute — a UI blip, a hit, a pickup. The\n * pool allocates once per source and replays.\n *\n * This is deliberately not {@link createAudioPlayer}: that handle tracks a\n * single \"current\" clip with loop, sink routing and lifecycle callbacks, which\n * is what background music needs. Effects are the opposite case — many\n * sources, all short, fire-and-forget, and the only thing that matters is that\n * firing one is cheap.\n *\n * A blocked `play()` is swallowed. Browsers reject playback until the user has\n * interacted with the page, and a sound effect is by definition not worth\n * interrupting anything over; call {@link SfxPool.preload} after the first\n * interaction if you want the pool warm.\n *\n * @param options - Master volume, base URL, voices per source and pool size.\n * @returns The pool handle.\n *\n * @example\n * const sfx = createSfxPool({ volume: 0.6, baseUrl: import.meta.env.BASE_URL });\n * sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function createSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { baseUrl = \"\", voices = 1, maxSources = 48 } = options;\n\n let master = clampVolume(options.volume ?? 1);\n const voiceCount = Math.max(1, Math.floor(voices));\n\n /**\n * Insertion-ordered by last play, so evicting the first key drops the\n * least recently used source.\n */\n const sources = new Map<string, { elements: HTMLAudioElement[]; next: number }>();\n\n /**\n * Per-play gain of whatever each element last played at.\n *\n * `setVolume` has to rescale live playback by the same factor it was\n * started with; assigning the master directly would yank a clip that\n * started at half volume up to full.\n */\n const gains = new WeakMap<HTMLAudioElement, number>();\n\n function resolve(src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n }\n\n function release(entry: { elements: HTMLAudioElement[] }): void {\n for (const element of entry.elements) {\n element.pause();\n element.removeAttribute(\"src\");\n element.load();\n }\n }\n\n function acquire(src: string): { elements: HTMLAudioElement[]; next: number } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n // Re-insert so the map order stays \"least recently played first\".\n sources.delete(url);\n sources.set(url, existing);\n return existing;\n }\n\n const entry = {\n elements: Array.from({ length: voiceCount }, () => {\n const element = new Audio(url);\n element.preload = \"auto\";\n return element;\n }),\n next: 0,\n };\n sources.set(url, entry);\n\n if (sources.size > maxSources) {\n const oldest = sources.keys().next();\n if (!oldest.done && oldest.value !== url) {\n const evicted = sources.get(oldest.value);\n if (evicted) release(evicted);\n sources.delete(oldest.value);\n }\n }\n\n return entry;\n }\n\n return {\n play(src, playOptions = {}) {\n const entry = acquire(src);\n if (!entry) return;\n\n const element = entry.elements[entry.next];\n entry.next = (entry.next + 1) % entry.elements.length;\n\n const gain = clampVolume(playOptions.volume ?? 1);\n gains.set(element, gain);\n\n element.pause();\n element.currentTime = 0;\n element.volume = clampVolume(gain * master);\n void element.play().catch(() => {});\n },\n\n preload(src) {\n for (const one of Array.isArray(src) ? src : [src]) {\n acquire(one)?.elements.forEach((element) => element.load());\n }\n },\n\n setVolume(volume) {\n master = clampVolume(volume);\n for (const entry of sources.values()) {\n for (const element of entry.elements) {\n if (element.paused) continue;\n element.volume = clampVolume((gains.get(element) ?? 1) * master);\n }\n }\n },\n\n stop(src) {\n const targets = src ? [sources.get(resolve(src))] : [...sources.values()];\n for (const entry of targets) {\n if (!entry) continue;\n for (const element of entry.elements) {\n element.pause();\n element.currentTime = 0;\n }\n }\n },\n\n dispose() {\n for (const entry of sources.values()) release(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":"AA+CA,SAAS,EAAY,EAAuB,CAExC,OADK,OAAO,SAAS,CAAK,EACnB,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,CAAK,CAAC,EADD,CAExC,CA8BA,SAAgB,EAAc,EAA0B,CAAC,EAAY,CACjE,GAAM,CAAE,UAAU,GAAI,SAAS,EAAG,aAAa,IAAO,EAElD,EAAS,EAAY,EAAQ,QAAU,CAAC,EACtC,EAAa,KAAK,IAAI,EAAG,KAAK,MAAM,CAAM,CAAC,EAM3C,EAAU,IAAI,IASd,EAAQ,IAAI,QAElB,SAAS,EAAQ,EAAqB,CAElC,MADI,CAAC,GAAW,kBAAkB,KAAK,CAAG,GAAK,EAAI,WAAW,OAAO,EAAU,EACxE,GAAG,EAAQ,QAAQ,MAAO,EAAE,EAAE,GAAG,EAAI,QAAQ,MAAO,EAAE,GACjE,CAEA,SAAS,EAAQ,EAA+C,CAC5D,IAAK,IAAM,KAAW,EAAM,SACxB,EAAQ,MAAM,EACd,EAAQ,gBAAgB,KAAK,EAC7B,EAAQ,KAAK,CAErB,CAEA,SAAS,EAAQ,EAAoE,CACjF,GAAI,OAAO,MAAU,IAAa,OAAO,KAEzC,IAAM,EAAM,EAAQ,CAAG,EACjB,EAAW,EAAQ,IAAI,CAAG,EAChC,GAAI,EAIA,OAFA,EAAQ,OAAO,CAAG,EAClB,EAAQ,IAAI,EAAK,CAAQ,EAClB,EAGX,IAAM,EAAQ,CACV,SAAU,MAAM,KAAK,CAAE,OAAQ,CAAW,MAAS,CAC/C,IAAM,EAAU,IAAI,MAAM,CAAG,EAE7B,MADA,GAAQ,QAAU,OACX,CACX,CAAC,EACD,KAAM,CACV,EAGA,GAFA,EAAQ,IAAI,EAAK,CAAK,EAElB,EAAQ,KAAO,EAAY,CAC3B,IAAM,EAAS,EAAQ,KAAK,CAAC,CAAC,KAAK,EACnC,GAAI,CAAC,EAAO,MAAQ,EAAO,QAAU,EAAK,CACtC,IAAM,EAAU,EAAQ,IAAI,EAAO,KAAK,EACpC,GAAS,EAAQ,CAAO,EAC5B,EAAQ,OAAO,EAAO,KAAK,CAC/B,CACJ,CAEA,OAAO,CACX,CAEA,MAAO,CACH,KAAK,EAAK,EAAc,CAAC,EAAG,CACxB,IAAM,EAAQ,EAAQ,CAAG,EACzB,GAAI,CAAC,EAAO,OAEZ,IAAM,EAAU,EAAM,SAAS,EAAM,MACrC,EAAM,MAAQ,EAAM,KAAO,GAAK,EAAM,SAAS,OAE/C,IAAM,EAAO,EAAY,EAAY,QAAU,CAAC,EAChD,EAAM,IAAI,EAAS,CAAI,EAEvB,EAAQ,MAAM,EACd,EAAQ,YAAc,EACtB,EAAQ,OAAS,EAAY,EAAO,CAAM,EAC1C,EAAa,KAAK,CAAC,CAAC,UAAY,CAAC,CAAC,CACtC,EAEA,QAAQ,EAAK,CACT,IAAK,IAAM,KAAO,MAAM,QAAQ,CAAG,EAAI,EAAM,CAAC,CAAG,EAC7C,EAAQ,CAAG,CAAC,EAAE,SAAS,QAAS,GAAY,EAAQ,KAAK,CAAC,CAElE,EAEA,UAAU,EAAQ,CACd,EAAS,EAAY,CAAM,EAC3B,IAAK,IAAM,KAAS,EAAQ,OAAO,EAC/B,IAAK,IAAM,KAAW,EAAM,SACpB,EAAQ,SACZ,EAAQ,OAAS,GAAa,EAAM,IAAI,CAAO,GAAK,GAAK,CAAM,EAG3E,EAEA,KAAK,EAAK,CACN,IAAM,EAAU,EAAM,CAAC,EAAQ,IAAI,EAAQ,CAAG,CAAC,CAAC,EAAI,CAAC,GAAG,EAAQ,OAAO,CAAC,EACxE,IAAK,IAAM,KAAS,EACX,KACL,IAAK,IAAM,KAAW,EAAM,SACxB,EAAQ,MAAM,EACd,EAAQ,YAAc,CAGlC,EAEA,SAAU,CACN,IAAK,IAAM,KAAS,EAAQ,OAAO,EAAG,EAAQ,CAAK,EACnD,EAAQ,MAAM,CAClB,CACJ,CACJ"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/audio/sfx-pool.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return Number.isFinite(e) ? Math.min(1, Math.max(0, e)) : 1;
|
|
4
|
+
}
|
|
5
|
+
function t(t = {}) {
|
|
6
|
+
let { baseUrl: n = "", voices: r = 1, maxSources: i = 48 } = t, a = e(t.volume ?? 1), o = Math.max(1, Math.floor(r)), s = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function l(e) {
|
|
8
|
+
return !n || /^(https?:)?\/\//.test(e) || e.startsWith("data:") ? e : `${n.replace(/\/$/, "")}/${e.replace(/^\//, "")}`;
|
|
9
|
+
}
|
|
10
|
+
function u(e) {
|
|
11
|
+
for (let t of e.elements) t.pause(), t.removeAttribute("src"), t.load();
|
|
12
|
+
}
|
|
13
|
+
function d(e) {
|
|
14
|
+
if (typeof Audio > "u") return null;
|
|
15
|
+
let t = l(e), n = s.get(t);
|
|
16
|
+
if (n) return s.delete(t), s.set(t, n), n;
|
|
17
|
+
let r = {
|
|
18
|
+
elements: Array.from({ length: o }, () => {
|
|
19
|
+
let e = new Audio(t);
|
|
20
|
+
return e.preload = "auto", e;
|
|
21
|
+
}),
|
|
22
|
+
next: 0
|
|
23
|
+
};
|
|
24
|
+
if (s.set(t, r), s.size > i) {
|
|
25
|
+
let e = s.keys().next();
|
|
26
|
+
if (!e.done && e.value !== t) {
|
|
27
|
+
let t = s.get(e.value);
|
|
28
|
+
t && u(t), s.delete(e.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return r;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
play(t, n = {}) {
|
|
35
|
+
let r = d(t);
|
|
36
|
+
if (!r) return;
|
|
37
|
+
let i = r.elements[r.next];
|
|
38
|
+
r.next = (r.next + 1) % r.elements.length;
|
|
39
|
+
let o = e(n.volume ?? 1);
|
|
40
|
+
c.set(i, o), i.pause(), i.currentTime = 0, i.volume = e(o * a), i.play().catch(() => {});
|
|
41
|
+
},
|
|
42
|
+
preload(e) {
|
|
43
|
+
for (let t of Array.isArray(e) ? e : [e]) d(t)?.elements.forEach((e) => e.load());
|
|
44
|
+
},
|
|
45
|
+
setVolume(t) {
|
|
46
|
+
a = e(t);
|
|
47
|
+
for (let t of s.values()) for (let n of t.elements) n.paused || (n.volume = e((c.get(n) ?? 1) * a));
|
|
48
|
+
},
|
|
49
|
+
stop(e) {
|
|
50
|
+
let t = e ? [s.get(l(e))] : [...s.values()];
|
|
51
|
+
for (let e of t) if (e) for (let t of e.elements) t.pause(), t.currentTime = 0;
|
|
52
|
+
},
|
|
53
|
+
dispose() {
|
|
54
|
+
for (let e of s.values()) u(e);
|
|
55
|
+
s.clear();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { t as createSfxPool };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=sfx-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sfx-pool.js","names":[],"sources":["../../src/audio/sfx-pool.ts"],"sourcesContent":["/** Options for {@link createSfxPool}. */\nexport interface SfxPoolOptions {\n /**\n * Master volume, `0`–`1`, multiplied into every per-play volume. Default\n * `1`. Wire it to the app's sound setting so one value governs the lot.\n */\n volume?: number;\n /**\n * Prefix applied to sources that are not absolute URLs — typically Vite's\n * `import.meta.env.BASE_URL`, so a build served from a subpath resolves.\n * Default `\"\"`.\n */\n baseUrl?: string;\n /**\n * Elements kept per source. `1` (the default) restarts the clip on every\n * play, which is what a menu blip wants. Raise it to let a sound overlap\n * itself — a hit landing while the previous one is still ringing.\n */\n voices?: number;\n /**\n * Maximum number of distinct sources held. When exceeded, the least\n * recently played source is released. Default `48`.\n */\n maxSources?: number;\n}\n\n/** Per-play overrides. */\nexport interface PlaySfxOptions {\n /** Volume for this play, `0`–`1`, multiplied by the pool's master. Default `1`. */\n volume?: number;\n}\n\n/** Imperative handle over a pool of short sound effects. */\nexport interface SfxPool {\n /** Play a clip, allocating and caching its element on first use. */\n play: (src: string, options?: PlaySfxOptions) => void;\n /** Fetch clips ahead of the first play, so it is not silent while the file downloads. */\n preload: (src: string | string[]) => void;\n /** Set the master volume, applying it to anything already sounding. */\n setVolume: (volume: number) => void;\n /** Stop one source, or every source when called with no argument. */\n stop: (src?: string) => void;\n /** Release every element. Call on unmount. */\n dispose: () => void;\n}\n\n/** Clamp to the range `HTMLMediaElement.volume` accepts, rejecting `NaN`. */\nfunction clampVolume(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * A pool of preallocated `<audio>` elements for short sound effects.\n *\n * `new Audio(src)` on every play allocates an element and re-enters the network\n * stack for a file the browser already has, which is the wrong shape for a\n * sound that fires dozens of times a minute — a UI blip, a hit, a pickup. The\n * pool allocates once per source and replays.\n *\n * This is deliberately not {@link createAudioPlayer}: that handle tracks a\n * single \"current\" clip with loop, sink routing and lifecycle callbacks, which\n * is what background music needs. Effects are the opposite case — many\n * sources, all short, fire-and-forget, and the only thing that matters is that\n * firing one is cheap.\n *\n * A blocked `play()` is swallowed. Browsers reject playback until the user has\n * interacted with the page, and a sound effect is by definition not worth\n * interrupting anything over; call {@link SfxPool.preload} after the first\n * interaction if you want the pool warm.\n *\n * @param options - Master volume, base URL, voices per source and pool size.\n * @returns The pool handle.\n *\n * @example\n * const sfx = createSfxPool({ volume: 0.6, baseUrl: import.meta.env.BASE_URL });\n * sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function createSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { baseUrl = \"\", voices = 1, maxSources = 48 } = options;\n\n let master = clampVolume(options.volume ?? 1);\n const voiceCount = Math.max(1, Math.floor(voices));\n\n /**\n * Insertion-ordered by last play, so evicting the first key drops the\n * least recently used source.\n */\n const sources = new Map<string, { elements: HTMLAudioElement[]; next: number }>();\n\n /**\n * Per-play gain of whatever each element last played at.\n *\n * `setVolume` has to rescale live playback by the same factor it was\n * started with; assigning the master directly would yank a clip that\n * started at half volume up to full.\n */\n const gains = new WeakMap<HTMLAudioElement, number>();\n\n function resolve(src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n }\n\n function release(entry: { elements: HTMLAudioElement[] }): void {\n for (const element of entry.elements) {\n element.pause();\n element.removeAttribute(\"src\");\n element.load();\n }\n }\n\n function acquire(src: string): { elements: HTMLAudioElement[]; next: number } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n // Re-insert so the map order stays \"least recently played first\".\n sources.delete(url);\n sources.set(url, existing);\n return existing;\n }\n\n const entry = {\n elements: Array.from({ length: voiceCount }, () => {\n const element = new Audio(url);\n element.preload = \"auto\";\n return element;\n }),\n next: 0,\n };\n sources.set(url, entry);\n\n if (sources.size > maxSources) {\n const oldest = sources.keys().next();\n if (!oldest.done && oldest.value !== url) {\n const evicted = sources.get(oldest.value);\n if (evicted) release(evicted);\n sources.delete(oldest.value);\n }\n }\n\n return entry;\n }\n\n return {\n play(src, playOptions = {}) {\n const entry = acquire(src);\n if (!entry) return;\n\n const element = entry.elements[entry.next];\n entry.next = (entry.next + 1) % entry.elements.length;\n\n const gain = clampVolume(playOptions.volume ?? 1);\n gains.set(element, gain);\n\n element.pause();\n element.currentTime = 0;\n element.volume = clampVolume(gain * master);\n void element.play().catch(() => {});\n },\n\n preload(src) {\n for (const one of Array.isArray(src) ? src : [src]) {\n acquire(one)?.elements.forEach((element) => element.load());\n }\n },\n\n setVolume(volume) {\n master = clampVolume(volume);\n for (const entry of sources.values()) {\n for (const element of entry.elements) {\n if (element.paused) continue;\n element.volume = clampVolume((gains.get(element) ?? 1) * master);\n }\n }\n },\n\n stop(src) {\n const targets = src ? [sources.get(resolve(src))] : [...sources.values()];\n for (const entry of targets) {\n if (!entry) continue;\n for (const element of entry.elements) {\n element.pause();\n element.currentTime = 0;\n }\n }\n },\n\n dispose() {\n for (const entry of sources.values()) release(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":";AA+CA,SAAS,EAAY,GAAuB;CAExC,OADK,OAAO,SAAS,CAAK,IACnB,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,CAAK,CAAC,IADD;AAExC;AA8BA,SAAgB,EAAc,IAA0B,CAAC,GAAY;CACjE,IAAM,EAAE,aAAU,IAAI,YAAS,GAAG,gBAAa,OAAO,GAElD,IAAS,EAAY,EAAQ,UAAU,CAAC,GACtC,IAAa,KAAK,IAAI,GAAG,KAAK,MAAM,CAAM,CAAC,GAM3C,oBAAU,IAAI,IAA4D,GAS1E,oBAAQ,IAAI,QAAkC;CAEpD,SAAS,EAAQ,GAAqB;EAElC,OADI,CAAC,KAAW,kBAAkB,KAAK,CAAG,KAAK,EAAI,WAAW,OAAO,IAAU,IACxE,GAAG,EAAQ,QAAQ,OAAO,EAAE,EAAE,GAAG,EAAI,QAAQ,OAAO,EAAE;CACjE;CAEA,SAAS,EAAQ,GAA+C;EAC5D,KAAK,IAAM,KAAW,EAAM,UAGxB,AAFA,EAAQ,MAAM,GACd,EAAQ,gBAAgB,KAAK,GAC7B,EAAQ,KAAK;CAErB;CAEA,SAAS,EAAQ,GAAoE;EACjF,IAAI,OAAO,QAAU,KAAa,OAAO;EAEzC,IAAM,IAAM,EAAQ,CAAG,GACjB,IAAW,EAAQ,IAAI,CAAG;EAChC,IAAI,GAIA,OAFA,EAAQ,OAAO,CAAG,GAClB,EAAQ,IAAI,GAAK,CAAQ,GAClB;EAGX,IAAM,IAAQ;GACV,UAAU,MAAM,KAAK,EAAE,QAAQ,EAAW,SAAS;IAC/C,IAAM,IAAU,IAAI,MAAM,CAAG;IAE7B,OADA,EAAQ,UAAU,QACX;GACX,CAAC;GACD,MAAM;EACV;EAGA,IAFA,EAAQ,IAAI,GAAK,CAAK,GAElB,EAAQ,OAAO,GAAY;GAC3B,IAAM,IAAS,EAAQ,KAAK,CAAC,CAAC,KAAK;GACnC,IAAI,CAAC,EAAO,QAAQ,EAAO,UAAU,GAAK;IACtC,IAAM,IAAU,EAAQ,IAAI,EAAO,KAAK;IAExC,AADI,KAAS,EAAQ,CAAO,GAC5B,EAAQ,OAAO,EAAO,KAAK;GAC/B;EACJ;EAEA,OAAO;CACX;CAEA,OAAO;EACH,KAAK,GAAK,IAAc,CAAC,GAAG;GACxB,IAAM,IAAQ,EAAQ,CAAG;GACzB,IAAI,CAAC,GAAO;GAEZ,IAAM,IAAU,EAAM,SAAS,EAAM;GACrC,EAAM,QAAQ,EAAM,OAAO,KAAK,EAAM,SAAS;GAE/C,IAAM,IAAO,EAAY,EAAY,UAAU,CAAC;GAMhD,AALA,EAAM,IAAI,GAAS,CAAI,GAEvB,EAAQ,MAAM,GACd,EAAQ,cAAc,GACtB,EAAQ,SAAS,EAAY,IAAO,CAAM,GAC1C,EAAa,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC;EACtC;EAEA,QAAQ,GAAK;GACT,KAAK,IAAM,KAAO,MAAM,QAAQ,CAAG,IAAI,IAAM,CAAC,CAAG,GAC7C,EAAQ,CAAG,CAAC,EAAE,SAAS,SAAS,MAAY,EAAQ,KAAK,CAAC;EAElE;EAEA,UAAU,GAAQ;GACd,IAAS,EAAY,CAAM;GAC3B,KAAK,IAAM,KAAS,EAAQ,OAAO,GAC/B,KAAK,IAAM,KAAW,EAAM,UACpB,EAAQ,WACZ,EAAQ,SAAS,GAAa,EAAM,IAAI,CAAO,KAAK,KAAK,CAAM;EAG3E;EAEA,KAAK,GAAK;GACN,IAAM,IAAU,IAAM,CAAC,EAAQ,IAAI,EAAQ,CAAG,CAAC,CAAC,IAAI,CAAC,GAAG,EAAQ,OAAO,CAAC;GACxE,KAAK,IAAM,KAAS,GACX,OACL,KAAK,IAAM,KAAW,EAAM,UAExB,AADA,EAAQ,MAAM,GACd,EAAQ,cAAc;EAGlC;EAEA,UAAU;GACN,KAAK,IAAM,KAAS,EAAQ,OAAO,GAAG,EAAQ,CAAK;GACnD,EAAQ,MAAM;EAClB;CACJ;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./sfx-pool.cjs");let t=require("react");function n(n={}){let{volume:r=1,baseUrl:i,voices:a,maxSources:o}=n,[s]=(0,t.useState)(()=>e.createSfxPool({baseUrl:i,voices:a,maxSources:o,volume:r}));return(0,t.useEffect)(()=>{s.setVolume(r)},[s,r]),(0,t.useEffect)(()=>()=>s.dispose(),[s]),s}exports.useSfxPool=n;
|
|
2
|
+
//# sourceMappingURL=use-sfx-pool.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-sfx-pool.cjs","names":[],"sources":["../../src/audio/use-sfx-pool.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createSfxPool, type SfxPool, type SfxPoolOptions } from \"./sfx-pool\";\n\n/**\n * Component-scoped {@link createSfxPool}, with the lifecycle wired up.\n *\n * The pool is created once — through a lazy `useState` initializer, so no\n * element is allocated on a render that gets discarded — and disposed on\n * unmount, so its elements do not outlive the screen that owns them.\n *\n * `volume` is tracked separately: changing it calls `setVolume` on the existing\n * pool rather than rebuilding it, which would throw away every clip the user\n * has already downloaded — exactly the cost the pool exists to avoid.\n * `baseUrl`, `voices` and `maxSources` are read once, at creation.\n *\n * @param options - Passed through to {@link createSfxPool}.\n * @returns A stable pool handle.\n *\n * @example\n * const sfx = useSfxPool({ volume: settings.sfxVolume / 100, baseUrl: import.meta.env.BASE_URL });\n *\n * useEffect(() => { sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]); }, [sfx]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function useSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { volume = 1, baseUrl, voices, maxSources } = options;\n\n const [pool] = useState(() => createSfxPool({ baseUrl, voices, maxSources, volume }));\n\n useEffect(() => {\n pool.setVolume(volume);\n }, [pool, volume]);\n\n useEffect(() => () => pool.dispose(), [pool]);\n\n return pool;\n}\n"],"mappings":"yDAyBA,SAAgB,EAAW,EAA0B,CAAC,EAAY,CAC9D,GAAM,CAAE,SAAS,EAAG,UAAS,SAAQ,cAAe,EAE9C,CAAC,IAAA,EAAQ,EAAA,SAAA,KAAe,EAAA,cAAc,CAAE,UAAS,SAAQ,aAAY,QAAO,CAAC,CAAC,EAQpF,OANA,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAK,UAAU,CAAM,CACzB,EAAG,CAAC,EAAM,CAAM,CAAC,GAEjB,EAAA,EAAA,UAAA,SAAsB,EAAK,QAAQ,EAAG,CAAC,CAAI,CAAC,EAErC,CACX"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createSfxPool as e } from "./sfx-pool.js";
|
|
2
|
+
import { useEffect as t, useState as n } from "react";
|
|
3
|
+
//#region src/audio/use-sfx-pool.ts
|
|
4
|
+
function r(r = {}) {
|
|
5
|
+
let { volume: i = 1, baseUrl: a, voices: o, maxSources: s } = r, [c] = n(() => e({
|
|
6
|
+
baseUrl: a,
|
|
7
|
+
voices: o,
|
|
8
|
+
maxSources: s,
|
|
9
|
+
volume: i
|
|
10
|
+
}));
|
|
11
|
+
return t(() => {
|
|
12
|
+
c.setVolume(i);
|
|
13
|
+
}, [c, i]), t(() => () => c.dispose(), [c]), c;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { r as useSfxPool };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=use-sfx-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-sfx-pool.js","names":[],"sources":["../../src/audio/use-sfx-pool.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createSfxPool, type SfxPool, type SfxPoolOptions } from \"./sfx-pool\";\n\n/**\n * Component-scoped {@link createSfxPool}, with the lifecycle wired up.\n *\n * The pool is created once — through a lazy `useState` initializer, so no\n * element is allocated on a render that gets discarded — and disposed on\n * unmount, so its elements do not outlive the screen that owns them.\n *\n * `volume` is tracked separately: changing it calls `setVolume` on the existing\n * pool rather than rebuilding it, which would throw away every clip the user\n * has already downloaded — exactly the cost the pool exists to avoid.\n * `baseUrl`, `voices` and `maxSources` are read once, at creation.\n *\n * @param options - Passed through to {@link createSfxPool}.\n * @returns A stable pool handle.\n *\n * @example\n * const sfx = useSfxPool({ volume: settings.sfxVolume / 100, baseUrl: import.meta.env.BASE_URL });\n *\n * useEffect(() => { sfx.preload([\"sfx/select.mp3\", \"sfx/back.mp3\"]); }, [sfx]);\n *\n * <button onClick={() => sfx.play(\"sfx/select.mp3\")}>Confirmar</button>\n */\nexport function useSfxPool(options: SfxPoolOptions = {}): SfxPool {\n const { volume = 1, baseUrl, voices, maxSources } = options;\n\n const [pool] = useState(() => createSfxPool({ baseUrl, voices, maxSources, volume }));\n\n useEffect(() => {\n pool.setVolume(volume);\n }, [pool, volume]);\n\n useEffect(() => () => pool.dispose(), [pool]);\n\n return pool;\n}\n"],"mappings":";;;AAyBA,SAAgB,EAAW,IAA0B,CAAC,GAAY;CAC9D,IAAM,EAAE,YAAS,GAAG,YAAS,WAAQ,kBAAe,GAE9C,CAAC,KAAQ,QAAe,EAAc;EAAE;EAAS;EAAQ;EAAY;CAAO,CAAC,CAAC;CAQpF,OANA,QAAgB;EACZ,EAAK,UAAU,CAAM;CACzB,GAAG,CAAC,GAAM,CAAM,CAAC,GAEjB,cAAsB,EAAK,QAAQ,GAAG,CAAC,CAAI,CAAC,GAErC;AACX"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let e=require("react");function t(t,n={}){let{retries:r=3,initialDelay:i=400,reloadOnFinalFailure:a=!0}=n;async function o(e=1){try{return await t()}catch(t){if(e>=r)throw a&&typeof window<`u`&&window.location.reload(),t;return await new Promise(t=>setTimeout(t,i*2**(e-1))),o(e+1)}}return(0,e.lazy)(
|
|
1
|
+
let e=require("react");function t(t,n={}){let{retries:r=3,initialDelay:i=400,reloadOnFinalFailure:a=!0}=n;async function o(e=1){try{return await t()}catch(t){if(e>=r)throw a&&typeof window<`u`&&window.location.reload(),t;return await new Promise(t=>setTimeout(t,i*2**(e-1))),o(e+1)}}let s=null;function c(){return s||(s=o(),s.catch(()=>{s=null})),s}let l=(0,e.lazy)(c);return l.preload=c,l}exports.lazyWithRetry=t;
|
|
2
2
|
//# sourceMappingURL=lazy-with-retry.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-with-retry.cjs","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/**\n * Wrap `React.lazy` with automatic retry
|
|
1
|
+
{"version":3,"file":"lazy-with-retry.cjs","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/** A lazy component that can also be fetched ahead of being rendered. */\nexport type PreloadableLazy<T extends ComponentType<unknown>> = ReturnType<typeof lazy<T>> & {\n /**\n * Start fetching the chunk now, before anything renders it.\n *\n * Call it on the interaction that makes the route likely — hovering the\n * link, opening the menu that holds it, finishing the step before it — so\n * the chunk is warm by the time the user commits and the suspense fallback\n * never appears.\n *\n * Shares its work with the render path: whichever fires first performs the\n * single fetch and the other awaits the same promise. Safe to call\n * repeatedly.\n *\n * @returns The module, once loaded. Rejects when every retry failed; that\n * rejection is already handled internally, so a fire-and-forget call\n * never surfaces as an unhandled rejection.\n */\n preload: () => Promise<{ default: T }>;\n};\n\n/**\n * Wrap `React.lazy` with automatic retry and a `preload()` method.\n *\n * Common cause of failure: deployed-then-cached `index.html` references chunk\n * filenames that no longer exist. Retrying after a short delay typically picks\n * up the new bundle; a final `location.reload()` recovers from stale\n * `index.html`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n *\n * // Warm the chunk when the route becomes likely, not when it is needed.\n * <a href=\"/settings\" onMouseEnter={() => void Settings.preload()}>Settings</a>\n */\nexport function lazyWithRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): PreloadableLazy<T> {\n const { retries = 3, initialDelay = 400, reloadOnFinalFailure = true } = options;\n\n async function load(attempt = 1): Promise<{ default: T }> {\n try {\n return await factory();\n } catch (error) {\n if (attempt >= retries) {\n if (reloadOnFinalFailure && typeof window !== \"undefined\") {\n window.location.reload();\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, initialDelay * 2 ** (attempt - 1)));\n return load(attempt + 1);\n }\n }\n\n let pending: Promise<{ default: T }> | null = null;\n\n /**\n * Run `load` at most once at a time, so preloading and rendering never\n * fetch the same chunk twice.\n *\n * The internal `catch` does two jobs: it clears the memo, so a component\n * that failed can be tried again once an error boundary resets, and it\n * marks the promise as handled, so a `preload()` nobody awaited does not\n * raise an unhandled rejection. Callers still get a promise that rejects.\n */\n function loadOnce(): Promise<{ default: T }> {\n if (!pending) {\n pending = load();\n pending.catch(() => {\n pending = null;\n });\n }\n return pending;\n }\n\n const component = lazy(loadOnce) as PreloadableLazy<T>;\n component.preload = loadOnce;\n return component;\n}\n"],"mappings":"uBAmDA,SAAgB,EACZ,EACA,EAAgC,CAAC,EACf,CAClB,GAAM,CAAE,UAAU,EAAG,eAAe,IAAK,uBAAuB,IAAS,EAEzE,eAAe,EAAK,EAAU,EAA4B,CACtD,GAAI,CACA,OAAO,MAAM,EAAQ,CACzB,OAAS,EAAO,CACZ,GAAI,GAAW,EAIX,MAHI,GAAwB,OAAO,OAAW,KAC1C,OAAO,SAAS,OAAO,EAErB,EAGV,OADA,MAAM,IAAI,QAAS,GAAY,WAAW,EAAS,EAAe,IAAM,EAAU,EAAE,CAAC,EAC9E,EAAK,EAAU,CAAC,CAC3B,CACJ,CAEA,IAAI,EAA0C,KAW9C,SAAS,GAAoC,CAOzC,OANK,IACD,EAAU,EAAK,EACf,EAAQ,UAAY,CAChB,EAAU,IACd,CAAC,GAEE,CACX,CAEA,IAAM,GAAA,EAAY,EAAA,KAAA,CAAK,CAAQ,EAE/B,MADA,GAAU,QAAU,EACb,CACX"}
|
|
@@ -10,7 +10,14 @@ function t(t, n = {}) {
|
|
|
10
10
|
return await new Promise((t) => setTimeout(t, i * 2 ** (e - 1))), o(e + 1);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
let s = null;
|
|
14
|
+
function c() {
|
|
15
|
+
return s || (s = o(), s.catch(() => {
|
|
16
|
+
s = null;
|
|
17
|
+
})), s;
|
|
18
|
+
}
|
|
19
|
+
let l = e(c);
|
|
20
|
+
return l.preload = c, l;
|
|
14
21
|
}
|
|
15
22
|
//#endregion
|
|
16
23
|
export { t as lazyWithRetry };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lazy-with-retry.js","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/**\n * Wrap `React.lazy` with automatic retry
|
|
1
|
+
{"version":3,"file":"lazy-with-retry.js","names":[],"sources":["../../src/auth/lazy-with-retry.ts"],"sourcesContent":["import { lazy } from \"react\";\nimport type { ComponentType } from \"react\";\n\nexport interface LazyWithRetryOptions {\n /** Max attempts. Default: 3. */\n retries?: number;\n /** Initial delay (ms) before retrying. Default: 400. */\n initialDelay?: number;\n /**\n * Reload the page after every retry fails. Helps when the stale chunk\n * error is caused by an old `index.html` referencing deleted bundles.\n * Default: true.\n */\n reloadOnFinalFailure?: boolean;\n}\n\n/** A lazy component that can also be fetched ahead of being rendered. */\nexport type PreloadableLazy<T extends ComponentType<unknown>> = ReturnType<typeof lazy<T>> & {\n /**\n * Start fetching the chunk now, before anything renders it.\n *\n * Call it on the interaction that makes the route likely — hovering the\n * link, opening the menu that holds it, finishing the step before it — so\n * the chunk is warm by the time the user commits and the suspense fallback\n * never appears.\n *\n * Shares its work with the render path: whichever fires first performs the\n * single fetch and the other awaits the same promise. Safe to call\n * repeatedly.\n *\n * @returns The module, once loaded. Rejects when every retry failed; that\n * rejection is already handled internally, so a fire-and-forget call\n * never surfaces as an unhandled rejection.\n */\n preload: () => Promise<{ default: T }>;\n};\n\n/**\n * Wrap `React.lazy` with automatic retry and a `preload()` method.\n *\n * Common cause of failure: deployed-then-cached `index.html` references chunk\n * filenames that no longer exist. Retrying after a short delay typically picks\n * up the new bundle; a final `location.reload()` recovers from stale\n * `index.html`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n *\n * // Warm the chunk when the route becomes likely, not when it is needed.\n * <a href=\"/settings\" onMouseEnter={() => void Settings.preload()}>Settings</a>\n */\nexport function lazyWithRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): PreloadableLazy<T> {\n const { retries = 3, initialDelay = 400, reloadOnFinalFailure = true } = options;\n\n async function load(attempt = 1): Promise<{ default: T }> {\n try {\n return await factory();\n } catch (error) {\n if (attempt >= retries) {\n if (reloadOnFinalFailure && typeof window !== \"undefined\") {\n window.location.reload();\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, initialDelay * 2 ** (attempt - 1)));\n return load(attempt + 1);\n }\n }\n\n let pending: Promise<{ default: T }> | null = null;\n\n /**\n * Run `load` at most once at a time, so preloading and rendering never\n * fetch the same chunk twice.\n *\n * The internal `catch` does two jobs: it clears the memo, so a component\n * that failed can be tried again once an error boundary resets, and it\n * marks the promise as handled, so a `preload()` nobody awaited does not\n * raise an unhandled rejection. Callers still get a promise that rejects.\n */\n function loadOnce(): Promise<{ default: T }> {\n if (!pending) {\n pending = load();\n pending.catch(() => {\n pending = null;\n });\n }\n return pending;\n }\n\n const component = lazy(loadOnce) as PreloadableLazy<T>;\n component.preload = loadOnce;\n return component;\n}\n"],"mappings":";;AAmDA,SAAgB,EACZ,GACA,IAAgC,CAAC,GACf;CAClB,IAAM,EAAE,aAAU,GAAG,kBAAe,KAAK,0BAAuB,OAAS;CAEzE,eAAe,EAAK,IAAU,GAA4B;EACtD,IAAI;GACA,OAAO,MAAM,EAAQ;EACzB,SAAS,GAAO;GACZ,IAAI,KAAW,GAIX,MAHI,KAAwB,OAAO,SAAW,OAC1C,OAAO,SAAS,OAAO,GAErB;GAGV,OADA,MAAM,IAAI,SAAS,MAAY,WAAW,GAAS,IAAe,MAAM,IAAU,EAAE,CAAC,GAC9E,EAAK,IAAU,CAAC;EAC3B;CACJ;CAEA,IAAI,IAA0C;CAW9C,SAAS,IAAoC;EAOzC,OANK,MACD,IAAU,EAAK,GACf,EAAQ,YAAY;GAChB,IAAU;EACd,CAAC,IAEE;CACX;CAEA,IAAM,IAAY,EAAK,CAAQ;CAE/B,OADA,EAAU,UAAU,GACb;AACX"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../utils/cn.cjs"),t=require("./AppBar.module.cjs");let
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./use-sticky-body-warning.cjs"),n=require("./AppBar.module.cjs");let r=require("react/jsx-runtime"),i=require("lucide-react");function a({title:a,leading:o,showBack:s=!1,onBack:c,backLabel:l=`Go back`,backIcon:u,brand:d,actions:f,centered:p=!1,sticky:m=!0,tone:h=`surface`,bordered:g=!0,safeArea:_=!0,className:v,...y}){t.useStickyBodyWarning(m);let b=c??(()=>window.history.back()),x=o??(0,r.jsxs)(r.Fragment,{children:[s&&(0,r.jsx)(`button`,{type:`button`,className:n.default.iconBtn,onClick:b,"aria-label":l,children:u??(0,r.jsx)(i.ArrowLeft,{size:22,"aria-hidden":`true`})}),d&&(0,r.jsx)(`span`,{className:n.default.brand,children:d})]});return(0,r.jsxs)(`header`,{className:e.cn(n.default.bar,n.default[h],p&&n.default.centered,m&&n.default.sticky,g&&n.default.bordered,_&&n.default.safeArea,v),...y,children:[(0,r.jsx)(`div`,{className:n.default.leading,children:x}),a!=null&&a!==``&&(0,r.jsx)(`h1`,{className:n.default.title,"aria-live":`polite`,children:a}),(0,r.jsx)(`div`,{className:n.default.actions,children:f})]})}exports.AppBar=a;
|
|
2
2
|
//# sourceMappingURL=AppBar.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppBar.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n
|
|
1
|
+
{"version":3,"file":"AppBar.cjs","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport { useStickyBodyWarning } from \"./use-sticky-body-warning\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /**\n * Stick to the top of the scroll container. Default `true`.\n *\n * The page has to leave the body out of the scrolling: `body { overflow-x:\n * hidden }` makes the body a scroll container and the bar scrolls away with\n * the content. Use `overflow-x: clip` on `html` and `body` instead — in\n * development the bar says so in the console when it detects it.\n */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n useStickyBodyWarning(sticky);\n\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":"+KA+EA,SAAgB,EAAO,CACnB,QACA,UACA,WAAW,GACX,SACA,YAAY,UACZ,WACA,QACA,UACA,WAAW,GACX,SAAS,GACT,OAAO,UACP,WAAW,GACX,WAAW,GACX,YACA,GAAG,GACS,CACZ,EAAA,qBAAqB,CAAM,EAE3B,IAAM,EAAa,QAAiB,OAAO,QAAQ,KAAK,GAElD,EAAiB,IACnB,EAAA,EAAA,KAAA,CAAA,EAAA,SAAA,CAAA,SAAA,CACK,IACG,EAAA,EAAA,IAAA,CAAC,SAAD,CACI,KAAK,SACL,UAAW,EAAA,QAAO,QAClB,QAAS,EACT,aAAY,EAEX,SAAA,IAAY,EAAA,EAAA,IAAA,CAAC,EAAA,UAAD,CAAW,KAAM,GAAI,cAAY,MAAQ,CAAA,CAClD,CAAA,EAEX,IAAS,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,CAAY,CAAA,CACxD,CAAA,CAAA,EAGN,OACI,EAAA,EAAA,KAAA,CAAC,SAAD,CACI,UAAW,EAAA,GACP,EAAA,QAAO,IACP,EAAA,QAAO,GACP,GAAY,EAAA,QAAO,SACnB,GAAU,EAAA,QAAO,OACjB,GAAY,EAAA,QAAO,SACnB,GAAY,EAAA,QAAO,SACnB,CACJ,EACA,GAAI,EAVR,SAAA,EAYI,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAoB,CAAA,EACpD,GAAS,MAAQ,IAAU,KACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,QAAO,MAAO,YAAU,SAClC,SAAA,CACD,CAAA,GAER,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,QAAU,SAAA,CAAa,CAAA,CAC1C,GAEhB"}
|
|
@@ -1,43 +1,45 @@
|
|
|
1
1
|
import { cn as e } from "../../utils/cn.js";
|
|
2
|
-
import t from "./
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { useStickyBodyWarning as t } from "./use-sticky-body-warning.js";
|
|
3
|
+
import n from "./AppBar.module.js";
|
|
4
|
+
import { Fragment as r, jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
5
|
+
import { ArrowLeft as o } from "lucide-react";
|
|
5
6
|
//#region src/components/AppBar/AppBar.tsx
|
|
6
|
-
function
|
|
7
|
-
|
|
7
|
+
function s({ title: s, leading: c, showBack: l = !1, onBack: u, backLabel: d = "Go back", backIcon: f, brand: p, actions: m, centered: h = !1, sticky: g = !0, tone: _ = "surface", bordered: v = !0, safeArea: y = !0, className: b, ...x }) {
|
|
8
|
+
t(g);
|
|
9
|
+
let S = u ?? (() => window.history.back()), C = c ?? /* @__PURE__ */ a(r, { children: [l && /* @__PURE__ */ i("button", {
|
|
8
10
|
type: "button",
|
|
9
|
-
className:
|
|
10
|
-
onClick:
|
|
11
|
-
"aria-label":
|
|
12
|
-
children:
|
|
11
|
+
className: n.iconBtn,
|
|
12
|
+
onClick: S,
|
|
13
|
+
"aria-label": d,
|
|
14
|
+
children: f ?? /* @__PURE__ */ i(o, {
|
|
13
15
|
size: 22,
|
|
14
16
|
"aria-hidden": "true"
|
|
15
17
|
})
|
|
16
|
-
}),
|
|
17
|
-
className:
|
|
18
|
-
children:
|
|
18
|
+
}), p && /* @__PURE__ */ i("span", {
|
|
19
|
+
className: n.brand,
|
|
20
|
+
children: p
|
|
19
21
|
})] });
|
|
20
|
-
return /* @__PURE__ */
|
|
21
|
-
className: e(
|
|
22
|
-
...
|
|
22
|
+
return /* @__PURE__ */ a("header", {
|
|
23
|
+
className: e(n.bar, n[_], h && n.centered, g && n.sticky, v && n.bordered, y && n.safeArea, b),
|
|
24
|
+
...x,
|
|
23
25
|
children: [
|
|
24
|
-
/* @__PURE__ */
|
|
25
|
-
className:
|
|
26
|
-
children:
|
|
26
|
+
/* @__PURE__ */ i("div", {
|
|
27
|
+
className: n.leading,
|
|
28
|
+
children: C
|
|
27
29
|
}),
|
|
28
|
-
|
|
29
|
-
className:
|
|
30
|
+
s != null && s !== "" && /* @__PURE__ */ i("h1", {
|
|
31
|
+
className: n.title,
|
|
30
32
|
"aria-live": "polite",
|
|
31
|
-
children:
|
|
33
|
+
children: s
|
|
32
34
|
}),
|
|
33
|
-
/* @__PURE__ */
|
|
34
|
-
className:
|
|
35
|
-
children:
|
|
35
|
+
/* @__PURE__ */ i("div", {
|
|
36
|
+
className: n.actions,
|
|
37
|
+
children: m
|
|
36
38
|
})
|
|
37
39
|
]
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
//#endregion
|
|
41
|
-
export {
|
|
43
|
+
export { s as AppBar };
|
|
42
44
|
|
|
43
45
|
//# sourceMappingURL=AppBar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppBar.js","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n
|
|
1
|
+
{"version":3,"file":"AppBar.js","names":[],"sources":["../../../src/components/AppBar/AppBar.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — a top bar is a slot layout: leading (leading,\n * showBack, onBack, backLabel, backIcon, brand), centre (title, centered) and\n * trailing (actions), plus the chrome it sits in (sticky, tone, bordered, safeArea).\n * Every one of those is a real decision an app makes per screen, and the alternative\n * to accepting them is the app rebuilding the bar around the parts it wanted to\n * change.\n */\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { ArrowLeft } from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport { useStickyBodyWarning } from \"./use-sticky-body-warning\";\nimport styles from \"./AppBar.module.css\";\n\nexport type AppBarTone = \"surface\" | \"primary\" | \"transparent\";\n\nexport interface AppBarProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n /** Page title — string or any node. Rendered as the bar's `<h1>`. */\n title?: ReactNode;\n /**\n * Replace the whole left slot. When set, the auto back button and `brand`\n * are ignored — you own the leading content.\n */\n leading?: ReactNode;\n /** Show a back button in the left slot. Ignored when `leading` is set. */\n showBack?: boolean;\n /**\n * Back-button handler. Defaults to `window.history.back()`. With a router,\n * pass `onBack={() => navigate(-1)}`.\n */\n onBack?: () => void;\n /** Accessible label for the back button. Default `\"Go back\"`. */\n backLabel?: string;\n /** Custom back icon. Default a left arrow. */\n backIcon?: ReactNode;\n /** Brand / logo node shown in the left slot (after the back button). */\n brand?: ReactNode;\n /** Right slot — action buttons / menu. One node or many. */\n actions?: ReactNode;\n /** Center the title (three-column grid). Default `false` (left-aligned). */\n centered?: boolean;\n /**\n * Stick to the top of the scroll container. Default `true`.\n *\n * The page has to leave the body out of the scrolling: `body { overflow-x:\n * hidden }` makes the body a scroll container and the bar scrolls away with\n * the content. Use `overflow-x: clip` on `html` and `body` instead — in\n * development the bar says so in the console when it detects it.\n */\n sticky?: boolean;\n /** Visual tone. Default `\"surface\"`. */\n tone?: AppBarTone;\n /** Thin bottom border. Default `true`. */\n bordered?: boolean;\n /** Add `env(safe-area-inset-top)` padding (iOS notch / PWA). Default `true`. */\n safeArea?: boolean;\n}\n\n/**\n * Mobile-first top app bar for PWAs — leading (back / brand) + title +\n * trailing actions, sticky with safe-area padding out of the box.\n *\n * Consumers customise via tokens (`--tempest-*`) and slots; the SDK ships the\n * layout, sticky/safe-area behaviour, and accessible back button so apps don't\n * hand-roll one per screen. For a desktop three-slot nav use [[Navbar]].\n *\n * @example\n * // Detail screen with back + a trailing action\n * <AppBar\n * title=\"Profile\"\n * showBack\n * onBack={() => navigate(-1)}\n * actions={<IconButton icon={<Settings />} onClick={openSettings} />}\n * />\n *\n * @example\n * // Home screen — brand left, centered title disabled\n * <AppBar brand={<Logo />} actions={<UserMenu />} />\n */\nexport function AppBar({\n title,\n leading,\n showBack = false,\n onBack,\n backLabel = \"Go back\",\n backIcon,\n brand,\n actions,\n centered = false,\n sticky = true,\n tone = \"surface\",\n bordered = true,\n safeArea = true,\n className,\n ...props\n}: AppBarProps) {\n useStickyBodyWarning(sticky);\n\n const handleBack = onBack ?? (() => window.history.back());\n\n const leadingContent = leading ?? (\n <>\n {showBack && (\n <button\n type=\"button\"\n className={styles.iconBtn}\n onClick={handleBack}\n aria-label={backLabel}\n >\n {backIcon ?? <ArrowLeft size={22} aria-hidden=\"true\" />}\n </button>\n )}\n {brand && <span className={styles.brand}>{brand}</span>}\n </>\n );\n\n return (\n <header\n className={cn(\n styles.bar,\n styles[tone],\n centered && styles.centered,\n sticky && styles.sticky,\n bordered && styles.bordered,\n safeArea && styles.safeArea,\n className,\n )}\n {...props}\n >\n <div className={styles.leading}>{leadingContent}</div>\n {title != null && title !== \"\" && (\n <h1 className={styles.title} aria-live=\"polite\">\n {title}\n </h1>\n )}\n <div className={styles.actions}>{actions}</div>\n </header>\n );\n}\n"],"mappings":";;;;;;AA+EA,SAAgB,EAAO,EACnB,UACA,YACA,cAAW,IACX,WACA,eAAY,WACZ,aACA,UACA,YACA,cAAW,IACX,YAAS,IACT,UAAO,WACP,cAAW,IACX,cAAW,IACX,cACA,GAAG,KACS;CACZ,EAAqB,CAAM;CAE3B,IAAM,IAAa,YAAiB,OAAO,QAAQ,KAAK,IAElD,IAAiB,KACnB,kBAAA,GAAA,EAAA,UAAA,CACK,KACG,kBAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAO;EAClB,SAAS;EACT,cAAY;EAEX,UAAA,KAAY,kBAAC,GAAD;GAAW,MAAM;GAAI,eAAY;EAAQ,CAAA;CAClD,CAAA,GAEX,KAAS,kBAAC,QAAD;EAAM,WAAW,EAAO;EAAQ,UAAA;CAAY,CAAA,CACxD,EAAA,CAAA;CAGN,OACI,kBAAC,UAAD;EACI,WAAW,EACP,EAAO,KACP,EAAO,IACP,KAAY,EAAO,UACnB,KAAU,EAAO,QACjB,KAAY,EAAO,UACnB,KAAY,EAAO,UACnB,CACJ;EACA,GAAI;EAVR,UAAA;GAYI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAoB,CAAA;GACpD,KAAS,QAAQ,MAAU,MACxB,kBAAC,MAAD;IAAI,WAAW,EAAO;IAAO,aAAU;IAClC,UAAA;GACD,CAAA;GAER,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAU,UAAA;GAAa,CAAA;EAC1C;;AAEhB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("react");var t="[tempest-react-sdk] <AppBar sticky /> will not stick: `document.body` has `overflow-x: hidden`, which forces its computed `overflow-y` to `auto` and makes the body a scroll container. The bar then pins to the body's scrollport instead of the viewport and scrolls off the top with the content — most visibly on Chrome Android. Fix: `html, body { overflow-x: clip }`, on both elements, since neither clamps alone. Pass `sticky={false}` if the bar is meant to scroll away.";function n(){return!1}var r=!1;function i(i){(0,e.useEffect)(()=>{!i||r||!n()||typeof document>`u`||!document.body||getComputedStyle(document.body).overflowX===`hidden`&&(r=!0,console.warn(t))},[i])}exports.useStickyBodyWarning=i;
|
|
2
|
+
//# sourceMappingURL=use-sticky-body-warning.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-sticky-body-warning.cjs","names":[],"sources":["../../../src/components/AppBar/use-sticky-body-warning.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\n/**\n * The one line of the app's own CSS that silently defeats `sticky`.\n *\n * `body { overflow-x: hidden }` is how most apps stop a too-wide child from\n * panning the page sideways. It also, per CSS Overflow 3, forces the computed\n * `overflow-y` to `auto` — a value the author never wrote — which makes the\n * body a **scroll container**. Every sticky descendant then pins to the body's\n * scrollport instead of the viewport, and that scrollport travels with the\n * document: measured in Chromium at 390px with a long page, the bar sat at\n * `top: -900px` after scrolling 900px, i.e. off screen.\n *\n * Nothing about that reads as a CSS problem from the app's side. The bar still\n * reports `position: sticky` in DevTools, the markup is unchanged, and on\n * desktop the screens usually fit without scrolling so the bug never shows. It\n * surfaces on Chrome Android, where the collapsing URL bar makes every screen a\n * scrolling screen — and it gets reported as \"the app bar is broken\", because\n * that is what the user sees.\n *\n * Hence this warning: the SDK cannot fix it from inside the component (a sticky\n * element has no way to opt out of its scroll container, and a rule the SDK\n * ships in its reset loses to the app's own stylesheet, which is imported\n * after), but it can name the cause in one console line.\n *\n * `clip` is the fix because it clamps the same horizontal overflow without\n * creating a scroll container. It has to be on `html` **and** `body` — with it\n * on only one of them the document still panned to `x: 500` in the same\n * measurement.\n */\nconst MESSAGE =\n \"[tempest-react-sdk] <AppBar sticky /> will not stick: `document.body` has \" +\n \"`overflow-x: hidden`, which forces its computed `overflow-y` to `auto` and makes the \" +\n \"body a scroll container. The bar then pins to the body's scrollport instead of the \" +\n \"viewport and scrolls off the top with the content — most visibly on Chrome Android. \" +\n \"Fix: `html, body { overflow-x: clip }`, on both elements, since neither clamps alone. \" +\n \"Pass `sticky={false}` if the bar is meant to scroll away.\";\n\n/**\n * Whether the bundle was built for development.\n *\n * Guarded because the SDK also runs where `import.meta.env` does not exist — a\n * service-worker context, a Node test runner, a build-time script.\n */\nfunction isDev(): boolean {\n return Boolean(import.meta.env?.DEV);\n}\n\n/** Set once the warning has been printed, so re-renders do not bury the console. */\nlet warned = false;\n\n/**\n * Reset the warn-once latch. Test-only — the latch is module state, and a suite\n * that renders more than one bar would otherwise see the warning only once.\n */\nexport function resetStickyBodyWarning(): void {\n warned = false;\n}\n\n/**\n * Warn once, in development, when the page's own CSS will keep a sticky bar\n * from sticking.\n *\n * Reads the body's `overflow-x` rather than walking the ancestors, because that\n * single declaration is the whole failure mode: an app that genuinely wants the\n * body to scroll writes `overflow-y: auto` itself, and a sticky bar inside a\n * scroll container the app chose on purpose behaves exactly as intended. What\n * is never intentional is acquiring that scroll container as a side effect of\n * clamping the other axis.\n *\n * @param enabled - Whether the bar is actually sticky. A bar rendered with\n * `sticky={false}` has nothing to lose and stays quiet.\n */\nexport function useStickyBodyWarning(enabled: boolean): void {\n useEffect(() => {\n if (!enabled || warned || !isDev()) return;\n if (typeof document === \"undefined\" || !document.body) return;\n if (getComputedStyle(document.body).overflowX !== \"hidden\") return;\n warned = true;\n console.warn(MESSAGE);\n }, [enabled]);\n}\n"],"mappings":"uBA8BA,IAAM,EACF,wdAaJ,SAAS,GAAiB,CACtB,MAAO,CAAA,CACX,CAGA,IAAI,EAAS,GAwBb,SAAgB,EAAqB,EAAwB,EACzD,EAAA,EAAA,UAAA,KAAgB,CACR,CAAC,GAAW,GAAU,CAAC,EAAM,GAC7B,OAAO,SAAa,KAAe,CAAC,SAAS,MAC7C,iBAAiB,SAAS,IAAI,CAAC,CAAC,YAAc,WAClD,EAAS,GACT,QAAQ,KAAK,CAAO,EACxB,EAAG,CAAC,CAAO,CAAC,CAChB"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect as e } from "react";
|
|
2
|
+
//#region src/components/AppBar/use-sticky-body-warning.ts
|
|
3
|
+
var t = "[tempest-react-sdk] <AppBar sticky /> will not stick: `document.body` has `overflow-x: hidden`, which forces its computed `overflow-y` to `auto` and makes the body a scroll container. The bar then pins to the body's scrollport instead of the viewport and scrolls off the top with the content — most visibly on Chrome Android. Fix: `html, body { overflow-x: clip }`, on both elements, since neither clamps alone. Pass `sticky={false}` if the bar is meant to scroll away.";
|
|
4
|
+
function n() {
|
|
5
|
+
return !1;
|
|
6
|
+
}
|
|
7
|
+
var r = !1;
|
|
8
|
+
function i(i) {
|
|
9
|
+
e(() => {
|
|
10
|
+
!i || r || !n() || typeof document > "u" || !document.body || getComputedStyle(document.body).overflowX === "hidden" && (r = !0, console.warn(t));
|
|
11
|
+
}, [i]);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { i as useStickyBodyWarning };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=use-sticky-body-warning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-sticky-body-warning.js","names":[],"sources":["../../../src/components/AppBar/use-sticky-body-warning.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\n/**\n * The one line of the app's own CSS that silently defeats `sticky`.\n *\n * `body { overflow-x: hidden }` is how most apps stop a too-wide child from\n * panning the page sideways. It also, per CSS Overflow 3, forces the computed\n * `overflow-y` to `auto` — a value the author never wrote — which makes the\n * body a **scroll container**. Every sticky descendant then pins to the body's\n * scrollport instead of the viewport, and that scrollport travels with the\n * document: measured in Chromium at 390px with a long page, the bar sat at\n * `top: -900px` after scrolling 900px, i.e. off screen.\n *\n * Nothing about that reads as a CSS problem from the app's side. The bar still\n * reports `position: sticky` in DevTools, the markup is unchanged, and on\n * desktop the screens usually fit without scrolling so the bug never shows. It\n * surfaces on Chrome Android, where the collapsing URL bar makes every screen a\n * scrolling screen — and it gets reported as \"the app bar is broken\", because\n * that is what the user sees.\n *\n * Hence this warning: the SDK cannot fix it from inside the component (a sticky\n * element has no way to opt out of its scroll container, and a rule the SDK\n * ships in its reset loses to the app's own stylesheet, which is imported\n * after), but it can name the cause in one console line.\n *\n * `clip` is the fix because it clamps the same horizontal overflow without\n * creating a scroll container. It has to be on `html` **and** `body` — with it\n * on only one of them the document still panned to `x: 500` in the same\n * measurement.\n */\nconst MESSAGE =\n \"[tempest-react-sdk] <AppBar sticky /> will not stick: `document.body` has \" +\n \"`overflow-x: hidden`, which forces its computed `overflow-y` to `auto` and makes the \" +\n \"body a scroll container. The bar then pins to the body's scrollport instead of the \" +\n \"viewport and scrolls off the top with the content — most visibly on Chrome Android. \" +\n \"Fix: `html, body { overflow-x: clip }`, on both elements, since neither clamps alone. \" +\n \"Pass `sticky={false}` if the bar is meant to scroll away.\";\n\n/**\n * Whether the bundle was built for development.\n *\n * Guarded because the SDK also runs where `import.meta.env` does not exist — a\n * service-worker context, a Node test runner, a build-time script.\n */\nfunction isDev(): boolean {\n return Boolean(import.meta.env?.DEV);\n}\n\n/** Set once the warning has been printed, so re-renders do not bury the console. */\nlet warned = false;\n\n/**\n * Reset the warn-once latch. Test-only — the latch is module state, and a suite\n * that renders more than one bar would otherwise see the warning only once.\n */\nexport function resetStickyBodyWarning(): void {\n warned = false;\n}\n\n/**\n * Warn once, in development, when the page's own CSS will keep a sticky bar\n * from sticking.\n *\n * Reads the body's `overflow-x` rather than walking the ancestors, because that\n * single declaration is the whole failure mode: an app that genuinely wants the\n * body to scroll writes `overflow-y: auto` itself, and a sticky bar inside a\n * scroll container the app chose on purpose behaves exactly as intended. What\n * is never intentional is acquiring that scroll container as a side effect of\n * clamping the other axis.\n *\n * @param enabled - Whether the bar is actually sticky. A bar rendered with\n * `sticky={false}` has nothing to lose and stays quiet.\n */\nexport function useStickyBodyWarning(enabled: boolean): void {\n useEffect(() => {\n if (!enabled || warned || !isDev()) return;\n if (typeof document === \"undefined\" || !document.body) return;\n if (getComputedStyle(document.body).overflowX !== \"hidden\") return;\n warned = true;\n console.warn(MESSAGE);\n }, [enabled]);\n}\n"],"mappings":";;AA8BA,IAAM,IACF;AAaJ,SAAS,IAAiB;CACtB,OAAO,CAAA;AACX;AAGA,IAAI,IAAS;AAwBb,SAAgB,EAAqB,GAAwB;CACzD,QAAgB;EACR,CAAC,KAAW,KAAU,CAAC,EAAM,KAC7B,OAAO,WAAa,OAAe,CAAC,SAAS,QAC7C,iBAAiB,SAAS,IAAI,CAAC,CAAC,cAAc,aAClD,IAAS,IACT,QAAQ,KAAK,CAAO;CACxB,GAAG,CAAC,CAAO,CAAC;AAChB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../utils/cn.cjs"),t=require("./bar-list-model.cjs"),n=require("./BarList.module.cjs");let r=require("react/jsx-runtime");function i({items:i,valueFormatter:a,showPercentage:o=!1,sort:s=`desc`,max:c,otherLabel:l,className:u,...d}){let f=t.buildBarListRows(i,s,c,l);return(0,r.jsx)(`ul`,{className:e.cn(n.default.list,u),...d,children:f.map(e=>(0,r.jsxs)(`li`,{className:n.default.item,children:[(0,r.jsxs)(`div`,{className:n.default.line,children:[(0,r.jsx)(`span`,{className:n.default.label,children:e.label}),(0,r.jsxs)(`span`,{className:n.default.value,children:[a?a(e.value):e.value,o&&(0,r.jsxs)(`span`,{className:n.default.percentage,children:[Math.round(e.percentage),`%`]})]})]}),(0,r.jsx)(`div`,{className:n.default.track,"aria-hidden":`true`,children:(0,r.jsx)(`div`,{className:n.default.fill,style:{width:`${e.width}%`,backgroundColor:e.color??`var(--tempest-chart-${e.index%8+1})`}})})]},`${e.label}-${e.index}`))})}exports.BarList=i;
|
|
2
|
+
//# sourceMappingURL=BarList.cjs.map
|