tempest-react-sdk 0.43.0 → 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.
Files changed (121) hide show
  1. package/README.md +6 -2
  2. package/dist/app/AppProviders.cjs +1 -1
  3. package/dist/app/AppProviders.js +6 -6
  4. package/dist/audio/sfx-pool.cjs +2 -0
  5. package/dist/audio/sfx-pool.cjs.map +1 -0
  6. package/dist/audio/sfx-pool.js +62 -0
  7. package/dist/audio/sfx-pool.js.map +1 -0
  8. package/dist/audio/use-sfx-pool.cjs +2 -0
  9. package/dist/audio/use-sfx-pool.cjs.map +1 -0
  10. package/dist/audio/use-sfx-pool.js +18 -0
  11. package/dist/audio/use-sfx-pool.js.map +1 -0
  12. package/dist/auth/lazy-with-retry.cjs +1 -1
  13. package/dist/auth/lazy-with-retry.cjs.map +1 -1
  14. package/dist/auth/lazy-with-retry.js +8 -1
  15. package/dist/auth/lazy-with-retry.js.map +1 -1
  16. package/dist/components/AppBar/AppBar.cjs +1 -1
  17. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  18. package/dist/components/AppBar/AppBar.js +27 -25
  19. package/dist/components/AppBar/AppBar.js.map +1 -1
  20. package/dist/components/AppBar/use-sticky-body-warning.cjs +2 -0
  21. package/dist/components/AppBar/use-sticky-body-warning.cjs.map +1 -0
  22. package/dist/components/AppBar/use-sticky-body-warning.js +16 -0
  23. package/dist/components/AppBar/use-sticky-body-warning.js.map +1 -0
  24. package/dist/components/BarList/BarList.cjs +2 -0
  25. package/dist/components/BarList/BarList.cjs.map +1 -0
  26. package/dist/components/BarList/BarList.js +42 -0
  27. package/dist/components/BarList/BarList.js.map +1 -0
  28. package/dist/components/BarList/BarList.module.cjs +2 -0
  29. package/dist/components/BarList/BarList.module.cjs.map +1 -0
  30. package/dist/components/BarList/BarList.module.js +15 -0
  31. package/dist/components/BarList/BarList.module.js.map +1 -0
  32. package/dist/components/BarList/bar-list-model.cjs +2 -0
  33. package/dist/components/BarList/bar-list-model.cjs.map +1 -0
  34. package/dist/components/BarList/bar-list-model.js +18 -0
  35. package/dist/components/BarList/bar-list-model.js.map +1 -0
  36. package/dist/components/DataTable/DataTable.cjs +1 -1
  37. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  38. package/dist/components/DataTable/DataTable.js +141 -108
  39. package/dist/components/DataTable/DataTable.js.map +1 -1
  40. package/dist/components/DataTable/DataTable.module.cjs +1 -1
  41. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  42. package/dist/components/DataTable/DataTable.module.js +13 -10
  43. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  44. package/dist/components/DataTable/LoadingRows.cjs +2 -0
  45. package/dist/components/DataTable/LoadingRows.cjs.map +1 -0
  46. package/dist/components/DataTable/LoadingRows.js +18 -0
  47. package/dist/components/DataTable/LoadingRows.js.map +1 -0
  48. package/dist/components/DataTable/use-dev-warnings.cjs +2 -0
  49. package/dist/components/DataTable/use-dev-warnings.cjs.map +1 -0
  50. package/dist/components/DataTable/use-dev-warnings.js +17 -0
  51. package/dist/components/DataTable/use-dev-warnings.js.map +1 -0
  52. package/dist/components/FilterBar/filter-apply.cjs +2 -0
  53. package/dist/components/FilterBar/filter-apply.cjs.map +1 -0
  54. package/dist/components/FilterBar/filter-apply.js +76 -0
  55. package/dist/components/FilterBar/filter-apply.js.map +1 -0
  56. package/dist/components/FilterBar/filter-query.cjs +2 -0
  57. package/dist/components/FilterBar/filter-query.cjs.map +1 -0
  58. package/dist/components/FilterBar/filter-query.js +38 -0
  59. package/dist/components/FilterBar/filter-query.js.map +1 -0
  60. package/dist/hooks/use-countdown.cjs +2 -0
  61. package/dist/hooks/use-countdown.cjs.map +1 -0
  62. package/dist/hooks/use-countdown.js +18 -0
  63. package/dist/hooks/use-countdown.js.map +1 -0
  64. package/dist/hooks/use-latest-ref.cjs +2 -0
  65. package/dist/hooks/use-latest-ref.cjs.map +1 -0
  66. package/dist/hooks/use-latest-ref.js +10 -0
  67. package/dist/hooks/use-latest-ref.js.map +1 -0
  68. package/dist/hooks/use-typewriter.cjs +2 -0
  69. package/dist/hooks/use-typewriter.cjs.map +1 -0
  70. package/dist/hooks/use-typewriter.js +28 -0
  71. package/dist/hooks/use-typewriter.js.map +1 -0
  72. package/dist/http/describe-api-error.cjs +2 -0
  73. package/dist/http/describe-api-error.cjs.map +1 -0
  74. package/dist/http/describe-api-error.js +22 -0
  75. package/dist/http/describe-api-error.js.map +1 -0
  76. package/dist/http/use-describe-api-error.cjs +2 -0
  77. package/dist/http/use-describe-api-error.cjs.map +1 -0
  78. package/dist/http/use-describe-api-error.js +15 -0
  79. package/dist/http/use-describe-api-error.js.map +1 -0
  80. package/dist/i18n/I18nProvider.cjs +1 -1
  81. package/dist/i18n/I18nProvider.cjs.map +1 -1
  82. package/dist/i18n/I18nProvider.js +4 -1
  83. package/dist/i18n/I18nProvider.js.map +1 -1
  84. package/dist/query/QueryProvider.cjs +1 -1
  85. package/dist/query/QueryProvider.cjs.map +1 -1
  86. package/dist/query/QueryProvider.js +13 -12
  87. package/dist/query/QueryProvider.js.map +1 -1
  88. package/dist/query/retry-policy.cjs +2 -0
  89. package/dist/query/retry-policy.cjs.map +1 -0
  90. package/dist/query/retry-policy.js +10 -0
  91. package/dist/query/retry-policy.js.map +1 -0
  92. package/dist/styles.css +1 -1
  93. package/dist/tempest-react-sdk.cjs +1 -1
  94. package/dist/tempest-react-sdk.d.ts +788 -9
  95. package/dist/tempest-react-sdk.js +275 -261
  96. package/dist/utils/compressed-storage.cjs +2 -0
  97. package/dist/utils/compressed-storage.cjs.map +1 -0
  98. package/dist/utils/compressed-storage.js +52 -0
  99. package/dist/utils/compressed-storage.js.map +1 -0
  100. package/dist/utils/csv.cjs +4 -0
  101. package/dist/utils/csv.cjs.map +1 -0
  102. package/dist/utils/csv.js +25 -0
  103. package/dist/utils/csv.js.map +1 -0
  104. package/dist/utils/format.cjs +1 -1
  105. package/dist/utils/format.cjs.map +1 -1
  106. package/dist/utils/format.js +11 -4
  107. package/dist/utils/format.js.map +1 -1
  108. package/dist/utils/numbers.cjs +1 -1
  109. package/dist/utils/numbers.cjs.map +1 -1
  110. package/dist/utils/numbers.js +5 -2
  111. package/dist/utils/numbers.js.map +1 -1
  112. package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
  113. package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
  114. package/dist/vite/tempest-pwa-dev-sw.js +10 -5
  115. package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
  116. package/dist/vite/tempest-pwa-manifest.cjs +2 -2
  117. package/dist/vite/tempest-pwa-manifest.cjs.map +1 -1
  118. package/dist/vite/tempest-pwa-manifest.js +17 -13
  119. package/dist/vite/tempest-pwa-manifest.js.map +1 -1
  120. package/dist/vite.d.ts +19 -2
  121. package/package.json +1 -1
package/README.md CHANGED
@@ -258,7 +258,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
258
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 |
259
259
  | `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
260
260
  | `share` | `share`, `isShareSupported`, `shareOrDownloadBlob` (Web Share file → download fallback), types: `SharePayload`, `ShareResult`, `ShareOrDownloadOptions` |
261
- | `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) |
262
262
  | generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
263
263
  | Module | Exports |
264
264
  | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -295,7 +295,7 @@ Every module is re-exported from the package root — `import { Button, useDebou
295
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 |
296
296
  | `feature-flags` | `FeatureFlagsProvider`, `useFeatureFlag`, `useFlagValue`, `createInMemoryFlags`, `createGrowthBookFeatureFlagsAdapter`, `createLaunchDarklyFeatureFlagsAdapter`, types: `FeatureFlagsAdapter`, `FlagValue`, `GrowthBookLike`, `LDClientLike` |
297
297
  | `share` | `share`, `isShareSupported`, `shareOrDownloadBlob` (Web Share file → download fallback), types: `SharePayload`, `ShareResult`, `ShareOrDownloadOptions` |
298
- | `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) |
299
299
  | generic components | display (`CopyButton`, `RelativeTime`, `Money`, `TruncateText`, `VisuallyHidden`), headless (`Portal`, `ClickOutside`, `ConditionalWrapper`, `For`, `ErrorText`), media/content (`Image`, `DataList`, `DescriptionList`) |
300
300
 
301
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).
@@ -1751,20 +1751,24 @@ import {
1751
1751
  cn,
1752
1752
  formatCurrency,
1753
1753
  formatDate,
1754
+ formatDateForInput,
1754
1755
  formatDateTime,
1755
1756
  formatPhone,
1756
1757
  formatCPF,
1757
1758
  formatPercent,
1759
+ percentOf,
1758
1760
  storage,
1759
1761
  } from "tempest-react-sdk";
1760
1762
 
1761
1763
  cn("btn", isPrimary && "btn-primary", { "btn-disabled": disabled });
1762
1764
  formatCurrency(1234.5, "BRL"); // "R$ 1.234,50"
1763
1765
  formatDate("2026-05-17"); // "17/05/2026"
1766
+ formatDateForInput("2026-05-17T14:30Z"); // "2026-05-17" — the value <input type="date"> accepts
1764
1767
  formatDateTime("2026-05-17T14:30Z"); // "17/05/2026 11:30"
1765
1768
  formatPhone("11998765432"); // "(11) 99876-5432"
1766
1769
  formatCPF("12345678909"); // "123.456.789-09"
1767
1770
  formatPercent(0.1234, 1); // "12,3%"
1771
+ percentOf(3, 4); // 75 — and percentOf(3, 0) is 0, not NaN
1768
1772
 
1769
1773
  storage.set("draft", { title: "..." });
1770
1774
  const draft = storage.get<Draft>("draft");
@@ -1,2 +1,2 @@
1
- const e=require("../query/QueryProvider.cjs"),t=require("../theme/ThemeProvider.cjs"),n=require("../i18n/I18nProvider.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)(n.I18nProvider,{...c,children:u})),s!==!1&&(u=(0,i.jsx)(t.ThemeProvider,{...s||{},children:u})),o!==!1&&(u=(0,i.jsx)(e.QueryProvider,{...o||{},children:u})),l&&(u=(0,i.jsx)(r.ErrorBoundary,{...l,children:u})),(0,i.jsx)(i.Fragment,{children:u})}exports.AppProviders=a;
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
@@ -1,18 +1,18 @@
1
- import { QueryProvider as e } from "../query/QueryProvider.js";
2
- import { ThemeProvider as t } from "../theme/ThemeProvider.js";
3
- import { I18nProvider as n } from "../i18n/I18nProvider.js";
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(n, {
9
+ return l && (d = /* @__PURE__ */ a(e, {
10
10
  ...l,
11
11
  children: d
12
- })), c !== !1 && (d = /* @__PURE__ */ a(t, {
12
+ })), c !== !1 && (d = /* @__PURE__ */ a(n, {
13
13
  ...c || {},
14
14
  children: d
15
- })), s !== !1 && (d = /* @__PURE__ */ a(e, {
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)(o)}exports.lazyWithRetry=t;
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. Common cause of failure:\n * deployed-then-cached `index.html` references chunk filenames that no\n * longer exist. Retrying after a short delay typically picks up the new\n * bundle; a final `location.reload()` recovers from stale `index.html`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n */\nexport function lazyWithRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): ReturnType<typeof lazy<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 return lazy(load) as ReturnType<typeof lazy<T>>;\n}\n"],"mappings":"uBAyBA,SAAgB,EACZ,EACA,EAAgC,CAAC,EACP,CAC1B,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,OAAA,EAAO,EAAA,KAAA,CAAK,CAAI,CACpB"}
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
- return e(o);
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. Common cause of failure:\n * deployed-then-cached `index.html` references chunk filenames that no\n * longer exist. Retrying after a short delay typically picks up the new\n * bundle; a final `location.reload()` recovers from stale `index.html`.\n *\n * @example\n * const Settings = lazyWithRetry(() => import(\"./Settings\"));\n */\nexport function lazyWithRetry<T extends ComponentType<unknown>>(\n factory: () => Promise<{ default: T }>,\n options: LazyWithRetryOptions = {},\n): ReturnType<typeof lazy<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 return lazy(load) as ReturnType<typeof lazy<T>>;\n}\n"],"mappings":";;AAyBA,SAAgB,EACZ,GACA,IAAgC,CAAC,GACP;CAC1B,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,OAAO,EAAK,CAAI;AACpB"}
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 n=require("react/jsx-runtime"),r=require("lucide-react");function i({title:i,leading:a,showBack:o=!1,onBack:s,backLabel:c=`Go back`,backIcon:l,brand:u,actions:d,centered:f=!1,sticky:p=!0,tone:m=`surface`,bordered:h=!0,safeArea:g=!0,className:_,...v}){let y=s??(()=>window.history.back()),b=a??(0,n.jsxs)(n.Fragment,{children:[o&&(0,n.jsx)(`button`,{type:`button`,className:t.default.iconBtn,onClick:y,"aria-label":c,children:l??(0,n.jsx)(r.ArrowLeft,{size:22,"aria-hidden":`true`})}),u&&(0,n.jsx)(`span`,{className:t.default.brand,children:u})]});return(0,n.jsxs)(`header`,{className:e.cn(t.default.bar,t.default[m],f&&t.default.centered,p&&t.default.sticky,h&&t.default.bordered,g&&t.default.safeArea,_),...v,children:[(0,n.jsx)(`div`,{className:t.default.leading,children:b}),i!=null&&i!==``&&(0,n.jsx)(`h1`,{className:t.default.title,"aria-live":`polite`,children:i}),(0,n.jsx)(`div`,{className:t.default.actions,children:d})]})}exports.AppBar=i;
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 /** Stick to the top of the scroll container. Default `true`. */\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 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":"oIAuEA,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,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
+ {"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 "./AppBar.module.js";
3
- import { Fragment as n, jsx as r, jsxs as i } from "react/jsx-runtime";
4
- import { ArrowLeft as a } from "lucide-react";
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 o({ title: o, leading: s, showBack: c = !1, onBack: l, backLabel: u = "Go back", backIcon: d, brand: f, actions: p, centered: m = !1, sticky: h = !0, tone: g = "surface", bordered: _ = !0, safeArea: v = !0, className: y, ...b }) {
7
- let x = l ?? (() => window.history.back()), S = s ?? /* @__PURE__ */ i(n, { children: [c && /* @__PURE__ */ r("button", {
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: t.iconBtn,
10
- onClick: x,
11
- "aria-label": u,
12
- children: d ?? /* @__PURE__ */ r(a, {
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
- }), f && /* @__PURE__ */ r("span", {
17
- className: t.brand,
18
- children: f
18
+ }), p && /* @__PURE__ */ i("span", {
19
+ className: n.brand,
20
+ children: p
19
21
  })] });
20
- return /* @__PURE__ */ i("header", {
21
- className: e(t.bar, t[g], m && t.centered, h && t.sticky, _ && t.bordered, v && t.safeArea, y),
22
- ...b,
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__ */ r("div", {
25
- className: t.leading,
26
- children: S
26
+ /* @__PURE__ */ i("div", {
27
+ className: n.leading,
28
+ children: C
27
29
  }),
28
- o != null && o !== "" && /* @__PURE__ */ r("h1", {
29
- className: t.title,
30
+ s != null && s !== "" && /* @__PURE__ */ i("h1", {
31
+ className: n.title,
30
32
  "aria-live": "polite",
31
- children: o
33
+ children: s
32
34
  }),
33
- /* @__PURE__ */ r("div", {
34
- className: t.actions,
35
- children: p
35
+ /* @__PURE__ */ i("div", {
36
+ className: n.actions,
37
+ children: m
36
38
  })
37
39
  ]
38
40
  });
39
41
  }
40
42
  //#endregion
41
- export { o as AppBar };
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 /** Stick to the top of the scroll container. Default `true`. */\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 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":";;;;;AAuEA,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,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"}
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BarList.cjs","names":[],"sources":["../../../src/components/BarList/BarList.tsx"],"sourcesContent":["import type { HTMLAttributes } from \"react\";\n\nimport { cn } from \"@/utils/cn\";\n\nimport { buildBarListRows, type BarListItem, type BarListSort } from \"./bar-list-model\";\nimport styles from \"./BarList.module.css\";\n\nexport type { BarListItem, BarListSort } from \"./bar-list-model\";\n\nexport interface BarListProps extends Omit<HTMLAttributes<HTMLUListElement>, \"children\"> {\n /** The rows. Entries whose value is not finite are dropped. */\n items: readonly BarListItem[];\n /** Render the number. Defaults to the value as-is. */\n valueFormatter?: (value: number) => string;\n /** Show each row's share of the total next to its value. Default `false`. */\n showPercentage?: boolean;\n /** Ordering. Default `\"desc\"`, which is what a ranking means. */\n sort?: BarListSort;\n /** Keep at most this many rows. */\n max?: number;\n /**\n * Label for one aggregated row holding everything `max` cut off.\n *\n * Without it, `max` simply truncates. The aggregate row only appears when more\n * than one row was cut — collapsing a single row into \"others\" hides its name\n * for nothing.\n */\n otherLabel?: string;\n}\n\n/**\n * A ranked distribution: label, proportional bar, value, optional share.\n *\n * The most common chart on an admin panel, and the one the SDK kept sending\n * people to write by hand — `Progress` is a single bar and `Sparkline` is a time\n * series, so a \"users per plan\" block ended up reimplemented per screen, each\n * with its own CSS and its own `.sort()`.\n *\n * It is a **list**, not a picture: `<ul>` / `<li>` with the value written as\n * text, the bar `aria-hidden` behind it. A screen reader reads \"Free, 128, 62%\"\n * because that text is there, not because an `aria-label` restates a drawing.\n * Which is also why the label never sits on top of the bar — text over a tinted\n * fill has to be re-verified against that fill, and the SDK has been caught by\n * that twice.\n *\n * Bar width is relative to the **largest** row, so the biggest bar fills the\n * track; the percentage is the row's share of the **total**. They are different\n * numbers on purpose: a width scaled by the total leaves every bar short in a\n * long list, which is when the chart is needed most.\n *\n * @example\n * <BarList\n * items={[{ label: \"Free\", value: 128 }, { label: \"Pro\", value: 32 }]}\n * valueFormatter={(n) => `${n} ativos`}\n * showPercentage\n * max={5}\n * otherLabel=\"Outros\"\n * />\n *\n * @param props - Items plus presentation.\n * @returns The list.\n */\nexport function BarList({\n items,\n valueFormatter,\n showPercentage = false,\n sort = \"desc\",\n max,\n otherLabel,\n className,\n ...rest\n}: BarListProps) {\n const rows = buildBarListRows(items, sort, max, otherLabel);\n\n return (\n <ul className={cn(styles.list, className)} {...rest}>\n {rows.map((row) => (\n <li key={`${row.label}-${row.index}`} className={styles.item}>\n <div className={styles.line}>\n <span className={styles.label}>{row.label}</span>\n <span className={styles.value}>\n {valueFormatter ? valueFormatter(row.value) : row.value}\n {showPercentage && (\n <span className={styles.percentage}>\n {Math.round(row.percentage)}%\n </span>\n )}\n </span>\n </div>\n <div className={styles.track} aria-hidden=\"true\">\n <div\n className={styles.fill}\n style={{\n width: `${row.width}%`,\n backgroundColor:\n row.color ?? `var(--tempest-chart-${(row.index % 8) + 1})`,\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"6IA8DA,SAAgB,EAAQ,CACpB,QACA,iBACA,iBAAiB,GACjB,OAAO,OACP,MACA,aACA,YACA,GAAG,GACU,CACb,IAAM,EAAO,EAAA,iBAAiB,EAAO,EAAM,EAAK,CAAU,EAE1D,OACI,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAW,EAAA,GAAG,EAAA,QAAO,KAAM,CAAS,EAAG,GAAI,EAC1C,SAAA,EAAK,IAAK,IACP,EAAA,EAAA,KAAA,CAAC,KAAD,CAAsC,UAAW,EAAA,QAAO,KAAxD,SAAA,EACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,KAAvB,SAAA,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAQ,SAAA,EAAI,KAAY,CAAA,GAChD,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,MAAxB,SAAA,CACK,EAAiB,EAAe,EAAI,KAAK,EAAI,EAAI,MACjD,IACG,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,WAAxB,SAAA,CACK,KAAK,MAAM,EAAI,UAAU,EAAE,GAC1B,CAER,CAAA,CAAA,CACL,CAAA,CAAA,CACL,CAAA,GAAA,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,MAAO,cAAY,OACtC,UAAA,EAAA,EAAA,IAAA,CAAC,MAAD,CACI,UAAW,EAAA,QAAO,KAClB,MAAO,CACH,MAAO,GAAG,EAAI,MAAM,GACpB,gBACI,EAAI,OAAS,uBAAwB,EAAI,MAAQ,EAAK,EAAE,EAChE,CACH,CAAA,CACA,CAAA,CACL,CAtBK,EAAA,GAAG,EAAI,MAAM,GAAG,EAAI,OAsBzB,CACP,CACD,CAAA,CAEZ"}
@@ -0,0 +1,42 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { buildBarListRows as t } from "./bar-list-model.js";
3
+ import n from "./BarList.module.js";
4
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
5
+ //#region src/components/BarList/BarList.tsx
6
+ function a({ items: a, valueFormatter: o, showPercentage: s = !1, sort: c = "desc", max: l, otherLabel: u, className: d, ...f }) {
7
+ let p = t(a, c, l, u);
8
+ return /* @__PURE__ */ r("ul", {
9
+ className: e(n.list, d),
10
+ ...f,
11
+ children: p.map((e) => /* @__PURE__ */ i("li", {
12
+ className: n.item,
13
+ children: [/* @__PURE__ */ i("div", {
14
+ className: n.line,
15
+ children: [/* @__PURE__ */ r("span", {
16
+ className: n.label,
17
+ children: e.label
18
+ }), /* @__PURE__ */ i("span", {
19
+ className: n.value,
20
+ children: [o ? o(e.value) : e.value, s && /* @__PURE__ */ i("span", {
21
+ className: n.percentage,
22
+ children: [Math.round(e.percentage), "%"]
23
+ })]
24
+ })]
25
+ }), /* @__PURE__ */ r("div", {
26
+ className: n.track,
27
+ "aria-hidden": "true",
28
+ children: /* @__PURE__ */ r("div", {
29
+ className: n.fill,
30
+ style: {
31
+ width: `${e.width}%`,
32
+ backgroundColor: e.color ?? `var(--tempest-chart-${e.index % 8 + 1})`
33
+ }
34
+ })
35
+ })]
36
+ }, `${e.label}-${e.index}`))
37
+ });
38
+ }
39
+ //#endregion
40
+ export { a as BarList };
41
+
42
+ //# sourceMappingURL=BarList.js.map