tempest-react-sdk 0.50.0 → 0.52.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 (77) hide show
  1. package/dist/audio/sfx-pool.cjs +1 -1
  2. package/dist/audio/sfx-pool.cjs.map +1 -1
  3. package/dist/audio/sfx-pool.js +36 -36
  4. package/dist/audio/sfx-pool.js.map +1 -1
  5. package/dist/auth/refresh-queue.cjs +1 -1
  6. package/dist/auth/refresh-queue.cjs.map +1 -1
  7. package/dist/auth/refresh-queue.js +6 -6
  8. package/dist/auth/refresh-queue.js.map +1 -1
  9. package/dist/components/BarList/BarList.cjs +1 -1
  10. package/dist/components/BarList/BarList.cjs.map +1 -1
  11. package/dist/components/BarList/BarList.js +6 -1
  12. package/dist/components/BarList/BarList.js.map +1 -1
  13. package/dist/components/BarList/bar-list-model.cjs +1 -1
  14. package/dist/components/BarList/bar-list-model.cjs.map +1 -1
  15. package/dist/components/BarList/bar-list-model.js +8 -8
  16. package/dist/components/BarList/bar-list-model.js.map +1 -1
  17. package/dist/components/Markdown/markdown-parse.cjs +5 -5
  18. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  19. package/dist/components/Markdown/markdown-parse.js +66 -63
  20. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  21. package/dist/components/Scheduler/Scheduler.cjs +1 -1
  22. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  23. package/dist/components/Scheduler/Scheduler.js +1 -1
  24. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  25. package/dist/components/Scheduler/Scheduler.module.cjs.map +1 -1
  26. package/dist/components/Scheduler/Scheduler.module.js.map +1 -1
  27. package/dist/components/Sidebar/Sidebar.cjs +1 -1
  28. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  29. package/dist/components/Sidebar/Sidebar.js +25 -45
  30. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  31. package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
  32. package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
  33. package/dist/components/Sidebar/SidebarEntry.cjs +2 -0
  34. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -0
  35. package/dist/components/Sidebar/SidebarEntry.js +40 -0
  36. package/dist/components/Sidebar/SidebarEntry.js.map +1 -0
  37. package/dist/http/api-client.cjs +1 -1
  38. package/dist/http/api-client.cjs.map +1 -1
  39. package/dist/http/api-client.js +84 -71
  40. package/dist/http/api-client.js.map +1 -1
  41. package/dist/http/describe-api-error.cjs +1 -1
  42. package/dist/http/describe-api-error.cjs.map +1 -1
  43. package/dist/http/describe-api-error.js +4 -2
  44. package/dist/http/describe-api-error.js.map +1 -1
  45. package/dist/http/timeout.cjs +2 -0
  46. package/dist/http/timeout.cjs.map +1 -0
  47. package/dist/http/timeout.js +22 -0
  48. package/dist/http/timeout.js.map +1 -0
  49. package/dist/http/use-describe-api-error.cjs +1 -1
  50. package/dist/http/use-describe-api-error.cjs.map +1 -1
  51. package/dist/http/use-describe-api-error.js +3 -2
  52. package/dist/http/use-describe-api-error.js.map +1 -1
  53. package/dist/icons/material-symbols.cjs.map +1 -1
  54. package/dist/icons/material-symbols.js.map +1 -1
  55. package/dist/offline/create-offline-database.cjs +2 -0
  56. package/dist/offline/create-offline-database.cjs.map +1 -0
  57. package/dist/offline/create-offline-database.js +29 -0
  58. package/dist/offline/create-offline-database.js.map +1 -0
  59. package/dist/offline/create-offline-store.cjs +1 -1
  60. package/dist/offline/create-offline-store.cjs.map +1 -1
  61. package/dist/offline/create-offline-store.js +29 -25
  62. package/dist/offline/create-offline-store.js.map +1 -1
  63. package/dist/styles.css +1 -1
  64. package/dist/tempest-react-sdk.cjs +1 -1
  65. package/dist/tempest-react-sdk.d.ts +286 -13
  66. package/dist/tempest-react-sdk.js +43 -42
  67. package/dist/utils/csv.cjs.map +1 -1
  68. package/dist/utils/csv.js.map +1 -1
  69. package/dist/utils/format.cjs +1 -1
  70. package/dist/utils/format.cjs.map +1 -1
  71. package/dist/utils/format.js +8 -3
  72. package/dist/utils/format.js.map +1 -1
  73. package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
  74. package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
  75. package/dist/vite/tempest-pwa-dev-sw.js +49 -41
  76. package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
  77. package/package.json +1 -1
@@ -1,2 +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),{entry:n,created:!1};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{entry:r,created:!0}}return{play(t,n={}){let r=d(t);if(!r)return;let i=r.entry,o=i.elements[i.next];i.next=(i.next+1)%i.elements.length;let s=e(n.volume??1);c.set(o,s),o.pause(),o.currentTime=0,o.volume=e(s*a),o.play().catch(()=>{})},preload(e){for(let t of Array.isArray(e)?e:[e]){let e=d(t);if(e?.created)for(let t of e.entry.elements)t.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;
1
+ function e(e){return Number.isFinite(e)?Math.min(1,Math.max(0,e)):1}function t(e,t){return!e||/^(https?:)?\/\//.test(t)||t.startsWith(`data:`)?t:`${e.replace(/\/$/,``)}/${t.replace(/^\//,``)}`}function n(e){for(let t of e.elements)t.pause(),t.removeAttribute(`src`),t.load()}function r(e,t,r){if(e.size<=r)return;let i=e.keys().next();if(i.done||i.value===t)return;let a=e.get(i.value);a&&n(a),e.delete(i.value)}function i(i={}){let{baseUrl:a=``,voices:o=1,maxSources:s=48}=i,c=e(i.volume??1),l=Math.max(1,Math.floor(o)),u=new Map,d=new WeakMap;function f(e){if(typeof Audio>`u`)return null;let n=t(a,e),i=u.get(n);if(i)return u.delete(n),u.set(n,i),{entry:i,created:!1};let o={elements:Array.from({length:l},()=>{let e=new Audio(n);return e.preload=`auto`,e}),next:0};return u.set(n,o),r(u,n,s),{entry:o,created:!0}}return{play(t,n={}){let r=f(t);if(!r)return;let i=r.entry,a=i.elements[i.next];i.next=(i.next+1)%i.elements.length;let o=e(n.volume??1);d.set(a,o),a.pause(),a.currentTime=0,a.volume=e(o*c),a.play().catch(()=>{})},preload(e){for(let t of Array.isArray(e)?e:[e]){let e=f(t);if(e?.created)for(let t of e.entry.elements)t.load()}},setVolume(t){c=e(t);for(let t of u.values())for(let n of t.elements)n.paused||(n.volume=e((d.get(n)??1)*c))},stop(e){let n=e?[u.get(t(a,e))]:[...u.values()];for(let e of n)if(e)for(let t of e.elements)t.pause(),t.currentTime=0},dispose(){for(let e of u.values())n(e);u.clear()}}}exports.createSfxPool=i;
2
2
  //# sourceMappingURL=sfx-pool.cjs.map
@@ -1 +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 */\n/** One source's voices plus the round-robin cursor over them. */\ninterface SourceEntry {\n /** The audio elements sharing this source. */\n elements: HTMLAudioElement[];\n /** Index of the element the next play claims. */\n next: number;\n}\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, SourceEntry>();\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 /**\n * The pool entry for a source, creating and admitting it when it is new.\n *\n * Re-inserts an existing entry so the map order stays \"least recently used\n * first\", which is what makes evicting the first key correct.\n *\n * `created` is reported because assigning `src` on an element whose `preload`\n * is `\"auto\"` already starts the fetch: calling `load()` on an entry that was\n * merely looked up would abort that fetch and discard whatever is already\n * buffered, per the media element load algorithm.\n *\n * @param src - Raw source, resolved against `baseUrl`.\n * @returns The entry and whether this call created it, or `null` where\n * `Audio` does not exist.\n */\n function acquire(src: string): { entry: SourceEntry; created: boolean } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n sources.delete(url);\n sources.set(url, existing);\n return { entry: existing, created: false };\n }\n\n const entry: SourceEntry = {\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, created: true };\n }\n\n return {\n play(src, playOptions = {}) {\n const acquired = acquire(src);\n if (!acquired) return;\n\n const entry = acquired.entry;\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 const acquired = acquire(one);\n if (!acquired?.created) continue;\n for (const element of acquired.entry.elements) 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":"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"}
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 */\n/** One source's voices plus the round-robin cursor over them. */\ninterface SourceEntry {\n /** The audio elements sharing this source. */\n elements: HTMLAudioElement[];\n /** Index of the element the next play claims. */\n next: number;\n}\n\n/**\n * Resolve a source against the pool's base URL.\n *\n * An absolute URL, a protocol-relative one and a `data:` URI are already\n * addresses — prefixing them would produce a path that resolves to nothing.\n *\n * @param baseUrl - Pool base, possibly empty.\n * @param src - Source as the caller wrote it.\n * @returns The URL to load.\n */\nfunction resolveSource(baseUrl: string, src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n}\n\n/**\n * Stop every voice of an entry and let the browser drop its buffer.\n *\n * Removing `src` before `load()` is what frees the decoded audio: `load()` on an\n * element that still has a source re-fetches it instead.\n *\n * @param entry - The entry being evicted.\n */\nfunction releaseEntry(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/**\n * Drop the least recently played source once the pool is over its cap.\n *\n * The map is insertion-ordered by last play, so the first key is the coldest.\n * The source that was just admitted is never the one evicted, which would\n * otherwise happen with a cap of one.\n *\n * @param sources - The live pool, mutated in place.\n * @param keepUrl - The source that must survive this eviction.\n * @param maxSources - How many sources the pool may hold.\n */\nfunction evictLeastRecent(\n sources: Map<string, SourceEntry>,\n keepUrl: string,\n maxSources: number,\n): void {\n if (sources.size <= maxSources) return;\n const oldest = sources.keys().next();\n if (oldest.done || oldest.value === keepUrl) return;\n const evicted = sources.get(oldest.value);\n if (evicted) releaseEntry(evicted);\n sources.delete(oldest.value);\n}\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, SourceEntry>();\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 /**\n * The pool entry for a source, creating and admitting it when it is new.\n *\n * Re-inserts an existing entry so the map order stays \"least recently used\n * first\", which is what makes evicting the first key correct.\n *\n * `created` is reported because assigning `src` on an element whose `preload`\n * is `\"auto\"` already starts the fetch: calling `load()` on an entry that was\n * merely looked up would abort that fetch and discard whatever is already\n * buffered, per the media element load algorithm.\n *\n * @param src - Raw source, resolved against `baseUrl`.\n * @returns The entry and whether this call created it, or `null` where\n * `Audio` does not exist.\n */\n function acquire(src: string): { entry: SourceEntry; created: boolean } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolveSource(baseUrl, src);\n const existing = sources.get(url);\n if (existing) {\n sources.delete(url);\n sources.set(url, existing);\n return { entry: existing, created: false };\n }\n\n const entry: SourceEntry = {\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 evictLeastRecent(sources, url, maxSources);\n\n return { entry, created: true };\n }\n\n return {\n play(src, playOptions = {}) {\n const acquired = acquire(src);\n if (!acquired) return;\n\n const entry = acquired.entry;\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 const acquired = acquire(one);\n if (!acquired?.created) continue;\n for (const element of acquired.entry.elements) 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\n ? [sources.get(resolveSource(baseUrl, src))]\n : [...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()) releaseEntry(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -2,70 +2,70 @@
2
2
  function e(e) {
3
3
  return Number.isFinite(e) ? Math.min(1, Math.max(0, e)) : 1;
4
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) {
5
+ function t(e, t) {
6
+ return !e || /^(https?:)?\/\//.test(t) || t.startsWith("data:") ? t : `${e.replace(/\/$/, "")}/${t.replace(/^\//, "")}`;
7
+ }
8
+ function n(e) {
9
+ for (let t of e.elements) t.pause(), t.removeAttribute("src"), t.load();
10
+ }
11
+ function r(e, t, r) {
12
+ if (e.size <= r) return;
13
+ let i = e.keys().next();
14
+ if (i.done || i.value === t) return;
15
+ let a = e.get(i.value);
16
+ a && n(a), e.delete(i.value);
17
+ }
18
+ function i(i = {}) {
19
+ let { baseUrl: a = "", voices: o = 1, maxSources: s = 48 } = i, c = e(i.volume ?? 1), l = Math.max(1, Math.floor(o)), u = /* @__PURE__ */ new Map(), d = /* @__PURE__ */ new WeakMap();
20
+ function f(e) {
14
21
  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), {
17
- entry: n,
22
+ let n = t(a, e), i = u.get(n);
23
+ if (i) return u.delete(n), u.set(n, i), {
24
+ entry: i,
18
25
  created: !1
19
26
  };
20
- let r = {
21
- elements: Array.from({ length: o }, () => {
22
- let e = new Audio(t);
27
+ let o = {
28
+ elements: Array.from({ length: l }, () => {
29
+ let e = new Audio(n);
23
30
  return e.preload = "auto", e;
24
31
  }),
25
32
  next: 0
26
33
  };
27
- if (s.set(t, r), s.size > i) {
28
- let e = s.keys().next();
29
- if (!e.done && e.value !== t) {
30
- let t = s.get(e.value);
31
- t && u(t), s.delete(e.value);
32
- }
33
- }
34
- return {
35
- entry: r,
34
+ return u.set(n, o), r(u, n, s), {
35
+ entry: o,
36
36
  created: !0
37
37
  };
38
38
  }
39
39
  return {
40
40
  play(t, n = {}) {
41
- let r = d(t);
41
+ let r = f(t);
42
42
  if (!r) return;
43
- let i = r.entry, o = i.elements[i.next];
43
+ let i = r.entry, a = i.elements[i.next];
44
44
  i.next = (i.next + 1) % i.elements.length;
45
- let s = e(n.volume ?? 1);
46
- c.set(o, s), o.pause(), o.currentTime = 0, o.volume = e(s * a), o.play().catch(() => {});
45
+ let o = e(n.volume ?? 1);
46
+ d.set(a, o), a.pause(), a.currentTime = 0, a.volume = e(o * c), a.play().catch(() => {});
47
47
  },
48
48
  preload(e) {
49
49
  for (let t of Array.isArray(e) ? e : [e]) {
50
- let e = d(t);
50
+ let e = f(t);
51
51
  if (e?.created) for (let t of e.entry.elements) t.load();
52
52
  }
53
53
  },
54
54
  setVolume(t) {
55
- a = e(t);
56
- for (let t of s.values()) for (let n of t.elements) n.paused || (n.volume = e((c.get(n) ?? 1) * a));
55
+ c = e(t);
56
+ for (let t of u.values()) for (let n of t.elements) n.paused || (n.volume = e((d.get(n) ?? 1) * c));
57
57
  },
58
58
  stop(e) {
59
- let t = e ? [s.get(l(e))] : [...s.values()];
60
- for (let e of t) if (e) for (let t of e.elements) t.pause(), t.currentTime = 0;
59
+ let n = e ? [u.get(t(a, e))] : [...u.values()];
60
+ for (let e of n) if (e) for (let t of e.elements) t.pause(), t.currentTime = 0;
61
61
  },
62
62
  dispose() {
63
- for (let e of s.values()) u(e);
64
- s.clear();
63
+ for (let e of u.values()) n(e);
64
+ u.clear();
65
65
  }
66
66
  };
67
67
  }
68
68
  //#endregion
69
- export { t as createSfxPool };
69
+ export { i as createSfxPool };
70
70
 
71
71
  //# sourceMappingURL=sfx-pool.js.map
@@ -1 +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 */\n/** One source's voices plus the round-robin cursor over them. */\ninterface SourceEntry {\n /** The audio elements sharing this source. */\n elements: HTMLAudioElement[];\n /** Index of the element the next play claims. */\n next: number;\n}\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, SourceEntry>();\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 /**\n * The pool entry for a source, creating and admitting it when it is new.\n *\n * Re-inserts an existing entry so the map order stays \"least recently used\n * first\", which is what makes evicting the first key correct.\n *\n * `created` is reported because assigning `src` on an element whose `preload`\n * is `\"auto\"` already starts the fetch: calling `load()` on an entry that was\n * merely looked up would abort that fetch and discard whatever is already\n * buffered, per the media element load algorithm.\n *\n * @param src - Raw source, resolved against `baseUrl`.\n * @returns The entry and whether this call created it, or `null` where\n * `Audio` does not exist.\n */\n function acquire(src: string): { entry: SourceEntry; created: boolean } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolve(src);\n const existing = sources.get(url);\n if (existing) {\n sources.delete(url);\n sources.set(url, existing);\n return { entry: existing, created: false };\n }\n\n const entry: SourceEntry = {\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, created: true };\n }\n\n return {\n play(src, playOptions = {}) {\n const acquired = acquire(src);\n if (!acquired) return;\n\n const entry = acquired.entry;\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 const acquired = acquire(one);\n if (!acquired?.created) continue;\n for (const element of acquired.entry.elements) 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;AAsCA,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,IAAyB,GASvC,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;CAiBA,SAAS,EAAQ,GAA8D;EAC3E,IAAI,OAAO,QAAU,KAAa,OAAO;EAEzC,IAAM,IAAM,EAAQ,CAAG,GACjB,IAAW,EAAQ,IAAI,CAAG;EAChC,IAAI,GAGA,OAFA,EAAQ,OAAO,CAAG,GAClB,EAAQ,IAAI,GAAK,CAAQ,GAClB;GAAE,OAAO;GAAU,SAAS;EAAM;EAG7C,IAAM,IAAqB;GACvB,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;GAAE;GAAO,SAAS;EAAK;CAClC;CAEA,OAAO;EACH,KAAK,GAAK,IAAc,CAAC,GAAG;GACxB,IAAM,IAAW,EAAQ,CAAG;GAC5B,IAAI,CAAC,GAAU;GAEf,IAAM,IAAQ,EAAS,OACjB,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,GAAG;IAChD,IAAM,IAAW,EAAQ,CAAG;IACvB,OAAU,SACf,KAAK,IAAM,KAAW,EAAS,MAAM,UAAU,EAAQ,KAAK;GAChE;EACJ;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"}
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 */\n/** One source's voices plus the round-robin cursor over them. */\ninterface SourceEntry {\n /** The audio elements sharing this source. */\n elements: HTMLAudioElement[];\n /** Index of the element the next play claims. */\n next: number;\n}\n\n/**\n * Resolve a source against the pool's base URL.\n *\n * An absolute URL, a protocol-relative one and a `data:` URI are already\n * addresses — prefixing them would produce a path that resolves to nothing.\n *\n * @param baseUrl - Pool base, possibly empty.\n * @param src - Source as the caller wrote it.\n * @returns The URL to load.\n */\nfunction resolveSource(baseUrl: string, src: string): string {\n if (!baseUrl || /^(https?:)?\\/\\//.test(src) || src.startsWith(\"data:\")) return src;\n return `${baseUrl.replace(/\\/$/, \"\")}/${src.replace(/^\\//, \"\")}`;\n}\n\n/**\n * Stop every voice of an entry and let the browser drop its buffer.\n *\n * Removing `src` before `load()` is what frees the decoded audio: `load()` on an\n * element that still has a source re-fetches it instead.\n *\n * @param entry - The entry being evicted.\n */\nfunction releaseEntry(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/**\n * Drop the least recently played source once the pool is over its cap.\n *\n * The map is insertion-ordered by last play, so the first key is the coldest.\n * The source that was just admitted is never the one evicted, which would\n * otherwise happen with a cap of one.\n *\n * @param sources - The live pool, mutated in place.\n * @param keepUrl - The source that must survive this eviction.\n * @param maxSources - How many sources the pool may hold.\n */\nfunction evictLeastRecent(\n sources: Map<string, SourceEntry>,\n keepUrl: string,\n maxSources: number,\n): void {\n if (sources.size <= maxSources) return;\n const oldest = sources.keys().next();\n if (oldest.done || oldest.value === keepUrl) return;\n const evicted = sources.get(oldest.value);\n if (evicted) releaseEntry(evicted);\n sources.delete(oldest.value);\n}\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, SourceEntry>();\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 /**\n * The pool entry for a source, creating and admitting it when it is new.\n *\n * Re-inserts an existing entry so the map order stays \"least recently used\n * first\", which is what makes evicting the first key correct.\n *\n * `created` is reported because assigning `src` on an element whose `preload`\n * is `\"auto\"` already starts the fetch: calling `load()` on an entry that was\n * merely looked up would abort that fetch and discard whatever is already\n * buffered, per the media element load algorithm.\n *\n * @param src - Raw source, resolved against `baseUrl`.\n * @returns The entry and whether this call created it, or `null` where\n * `Audio` does not exist.\n */\n function acquire(src: string): { entry: SourceEntry; created: boolean } | null {\n if (typeof Audio === \"undefined\") return null;\n\n const url = resolveSource(baseUrl, src);\n const existing = sources.get(url);\n if (existing) {\n sources.delete(url);\n sources.set(url, existing);\n return { entry: existing, created: false };\n }\n\n const entry: SourceEntry = {\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 evictLeastRecent(sources, url, maxSources);\n\n return { entry, created: true };\n }\n\n return {\n play(src, playOptions = {}) {\n const acquired = acquire(src);\n if (!acquired) return;\n\n const entry = acquired.entry;\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 const acquired = acquire(one);\n if (!acquired?.created) continue;\n for (const element of acquired.entry.elements) 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\n ? [sources.get(resolveSource(baseUrl, src))]\n : [...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()) releaseEntry(entry);\n sources.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- function e(e){let t=null;return()=>t||(t=(async()=>{try{await e()}finally{t=null}})(),t)}exports.createRefreshQueue=e;
1
+ function e(e,t={}){let{getToken:n}=t,r=null,i=null;return()=>r||(n&&i!==null&&n()===i?Promise.resolve():(r=(async()=>{try{await e(),n&&(i=n()??null)}finally{r=null}})(),r))}exports.createRefreshQueue=e;
2
2
  //# sourceMappingURL=refresh-queue.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-queue.cjs","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh());\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(refresh: () => Promise<void>): () => Promise<void> {\n let current: Promise<void> | null = null;\n\n return () => {\n if (current) return current;\n current = (async () => {\n try {\n await refresh();\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":"AAYA,SAAgB,EAAmB,EAAmD,CAClF,IAAI,EAAgC,KAEpC,UACQ,IACJ,GAAW,SAAY,CACnB,GAAI,CACA,MAAM,EAAQ,CAClB,QAAU,CACN,EAAU,IACd,CACJ,EAAA,CAAG,EACI,EAEf"}
1
+ {"version":3,"file":"refresh-queue.cjs","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["export interface CreateRefreshQueueOptions {\n /**\n * Reads the credential the refresh installs — typically the access token.\n *\n * Supplying it makes the queue skip a refresh whose work another caller has\n * already done. Without it, only calls that literally overlap in time are\n * collapsed.\n */\n getToken?: () => string | null | undefined;\n}\n\n/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * **In-flight sharing alone does not collapse a burst.** A page that fires\n * several requests at once gets several 401s back, but they do not land inside\n * one window: the stragglers arrive after the first refresh already resolved,\n * find no promise to join, and each starts another one — rotating a token that\n * is already fresh. Measured against a mock backend, five concurrent expired\n * requests took two refreshes, not one.\n *\n * Pass `getToken` to close that gap. The queue remembers the token its last\n * refresh produced, and a call that finds that same token still in place returns\n * immediately, because the refresh it was about to perform has already happened.\n * The same five requests then take exactly one refresh, and so do twenty.\n *\n * @param refresh - Performs the refresh and installs the new credentials.\n * @param options - Optional token reader that enables already-refreshed\n * detection.\n * @returns A function that refreshes at most once per rotation.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh(), {\n * getToken: () => useAuthStore.getState().token,\n * });\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(\n refresh: () => Promise<void>,\n options: CreateRefreshQueueOptions = {},\n): () => Promise<void> {\n const { getToken } = options;\n let current: Promise<void> | null = null;\n let lastIssued: string | null = null;\n\n return () => {\n if (current) return current;\n\n if (getToken && lastIssued !== null && getToken() === lastIssued) {\n return Promise.resolve();\n }\n\n current = (async () => {\n try {\n await refresh();\n if (getToken) lastIssued = getToken() ?? null;\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":"AA0CA,SAAgB,EACZ,EACA,EAAqC,CAAC,EACnB,CACnB,GAAM,CAAE,YAAa,EACjB,EAAgC,KAChC,EAA4B,KAEhC,UACQ,IAEA,GAAY,IAAe,MAAQ,EAAS,IAAM,EAC3C,QAAQ,QAAQ,GAG3B,GAAW,SAAY,CACnB,GAAI,CACA,MAAM,EAAQ,EACV,IAAU,EAAa,EAAS,GAAK,KAC7C,QAAU,CACN,EAAU,IACd,CACJ,EAAA,CAAG,EACI,GAEf"}
@@ -1,13 +1,13 @@
1
1
  //#region src/auth/refresh-queue.ts
2
- function e(e) {
3
- let t = null;
4
- return () => t || (t = (async () => {
2
+ function e(e, t = {}) {
3
+ let { getToken: n } = t, r = null, i = null;
4
+ return () => r || (n && i !== null && n() === i ? Promise.resolve() : (r = (async () => {
5
5
  try {
6
- await e();
6
+ await e(), n && (i = n() ?? null);
7
7
  } finally {
8
- t = null;
8
+ r = null;
9
9
  }
10
- })(), t);
10
+ })(), r));
11
11
  }
12
12
  //#endregion
13
13
  export { e as createRefreshQueue };
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-queue.js","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh());\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(refresh: () => Promise<void>): () => Promise<void> {\n let current: Promise<void> | null = null;\n\n return () => {\n if (current) return current;\n current = (async () => {\n try {\n await refresh();\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":";AAYA,SAAgB,EAAmB,GAAmD;CAClF,IAAI,IAAgC;CAEpC,aACQ,MACJ,KAAW,YAAY;EACnB,IAAI;GACA,MAAM,EAAQ;EAClB,UAAU;GACN,IAAU;EACd;CACJ,EAAA,CAAG,GACI;AAEf"}
1
+ {"version":3,"file":"refresh-queue.js","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["export interface CreateRefreshQueueOptions {\n /**\n * Reads the credential the refresh installs — typically the access token.\n *\n * Supplying it makes the queue skip a refresh whose work another caller has\n * already done. Without it, only calls that literally overlap in time are\n * collapsed.\n */\n getToken?: () => string | null | undefined;\n}\n\n/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * **In-flight sharing alone does not collapse a burst.** A page that fires\n * several requests at once gets several 401s back, but they do not land inside\n * one window: the stragglers arrive after the first refresh already resolved,\n * find no promise to join, and each starts another one — rotating a token that\n * is already fresh. Measured against a mock backend, five concurrent expired\n * requests took two refreshes, not one.\n *\n * Pass `getToken` to close that gap. The queue remembers the token its last\n * refresh produced, and a call that finds that same token still in place returns\n * immediately, because the refresh it was about to perform has already happened.\n * The same five requests then take exactly one refresh, and so do twenty.\n *\n * @param refresh - Performs the refresh and installs the new credentials.\n * @param options - Optional token reader that enables already-refreshed\n * detection.\n * @returns A function that refreshes at most once per rotation.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh(), {\n * getToken: () => useAuthStore.getState().token,\n * });\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(\n refresh: () => Promise<void>,\n options: CreateRefreshQueueOptions = {},\n): () => Promise<void> {\n const { getToken } = options;\n let current: Promise<void> | null = null;\n let lastIssued: string | null = null;\n\n return () => {\n if (current) return current;\n\n if (getToken && lastIssued !== null && getToken() === lastIssued) {\n return Promise.resolve();\n }\n\n current = (async () => {\n try {\n await refresh();\n if (getToken) lastIssued = getToken() ?? null;\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":";AA0CA,SAAgB,EACZ,GACA,IAAqC,CAAC,GACnB;CACnB,IAAM,EAAE,gBAAa,GACjB,IAAgC,MAChC,IAA4B;CAEhC,aACQ,MAEA,KAAY,MAAe,QAAQ,EAAS,MAAM,IAC3C,QAAQ,QAAQ,KAG3B,KAAW,YAAY;EACnB,IAAI;GAEA,AADA,MAAM,EAAQ,GACV,MAAU,IAAa,EAAS,KAAK;EAC7C,UAAU;GACN,IAAU;EACd;CACJ,EAAA,CAAG,GACI;AAEf"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/cn.cjs"),t=require("../../charts/use-chart-colors.cjs"),n=require("./bar-list-model.cjs"),r=require("./BarList.module.cjs");let i=require("react"),a=require("react/jsx-runtime");function o({items:o,valueFormatter:s,showPercentage:c=!1,sort:l=`desc`,max:u,otherLabel:d,className:f,...p}){let m=(0,i.useMemo)(()=>n.buildBarListRows(o,l,u,d),[o,l,u,d]),h=t.useChartColors();return(0,a.jsx)(`ul`,{className:e.cn(r.default.list,f),...p,children:m.map(e=>(0,a.jsxs)(`li`,{className:r.default.item,children:[(0,a.jsxs)(`div`,{className:r.default.line,children:[(0,a.jsx)(`span`,{className:r.default.label,children:e.label}),(0,a.jsxs)(`span`,{className:r.default.value,children:[s?s(e.value):e.value,c&&(0,a.jsxs)(`span`,{className:r.default.percentage,children:[Math.round(e.percentage),`%`]})]})]}),(0,a.jsx)(`div`,{className:r.default.track,"aria-hidden":`true`,children:(0,a.jsx)(`div`,{className:r.default.fill,style:{width:`${e.width}%`,backgroundColor:e.color??h[e.index%h.length]}})})]},`${e.label}-${e.index}`))})}exports.BarList=o;
1
+ const e=require("../../utils/cn.cjs"),t=require("../../charts/use-chart-colors.cjs"),n=require("./bar-list-model.cjs"),r=require("./BarList.module.cjs");let i=require("react"),a=require("react/jsx-runtime");function o({items:o,valueFormatter:s,showPercentage:c=!1,sort:l=`desc`,max:u,otherLabel:d,className:f,...p}){let m=(0,i.useMemo)(()=>n.buildBarListRows({items:o,sort:l,max:u,otherLabel:d}),[o,l,u,d]),h=t.useChartColors();return(0,a.jsx)(`ul`,{className:e.cn(r.default.list,f),...p,children:m.map(e=>(0,a.jsxs)(`li`,{className:r.default.item,children:[(0,a.jsxs)(`div`,{className:r.default.line,children:[(0,a.jsx)(`span`,{className:r.default.label,children:e.label}),(0,a.jsxs)(`span`,{className:r.default.value,children:[s?s(e.value):e.value,c&&(0,a.jsxs)(`span`,{className:r.default.percentage,children:[Math.round(e.percentage),`%`]})]})]}),(0,a.jsx)(`div`,{className:r.default.track,"aria-hidden":`true`,children:(0,a.jsx)(`div`,{className:r.default.fill,style:{width:`${e.width}%`,backgroundColor:e.color??h[e.index%h.length]}})})]},`${e.label}-${e.index}`))})}exports.BarList=o;
2
2
  //# sourceMappingURL=BarList.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"BarList.cjs","names":[],"sources":["../../../src/components/BarList/BarList.tsx"],"sourcesContent":["import { useMemo, type HTMLAttributes } from \"react\";\n\nimport { useChartColors } from \"@/charts/use-chart-colors\";\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 * Colors come from {@link useChartColors}, the same resolver every chart in the\n * SDK uses, so a theme that declares fewer series than the eight\n * `--tempest-chart-*` slots cycles within its own palette. Reading the tokens\n * through CSS `var()` could not do that: the count lives in\n * `--tempest-chart-count`, which CSS cannot use as a modulus, so a six-color\n * brand palette got the SDK's leftover defaults in rows seven and eight — the\n * exact regression that token exists to prevent. `palette.ts` imports nothing,\n * so this pulls no chart library into the slice.\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 = useMemo(\n () => buildBarListRows(items, sort, max, otherLabel),\n [items, sort, max, otherLabel],\n );\n const palette = useChartColors();\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: row.color ?? palette[row.index % palette.length],\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"+MAwEA,SAAgB,EAAQ,CACpB,QACA,iBACA,iBAAiB,GACjB,OAAO,OACP,MACA,aACA,YACA,GAAG,GACU,CACb,IAAM,GAAA,EAAO,EAAA,QAAA,KACH,EAAA,iBAAiB,EAAO,EAAM,EAAK,CAAU,EACnD,CAAC,EAAO,EAAM,EAAK,CAAU,CACjC,EACM,EAAU,EAAA,eAAe,EAE/B,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,gBAAiB,EAAI,OAAS,EAAQ,EAAI,MAAQ,EAAQ,OAC9D,CACH,CAAA,CACA,CAAA,CACL,CArBK,EAAA,GAAG,EAAI,MAAM,GAAG,EAAI,OAqBzB,CACP,CACD,CAAA,CAEZ"}
1
+ {"version":3,"file":"BarList.cjs","names":[],"sources":["../../../src/components/BarList/BarList.tsx"],"sourcesContent":["import { useMemo, type HTMLAttributes } from \"react\";\n\nimport { useChartColors } from \"@/charts/use-chart-colors\";\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 * Colors come from {@link useChartColors}, the same resolver every chart in the\n * SDK uses, so a theme that declares fewer series than the eight\n * `--tempest-chart-*` slots cycles within its own palette. Reading the tokens\n * through CSS `var()` could not do that: the count lives in\n * `--tempest-chart-count`, which CSS cannot use as a modulus, so a six-color\n * brand palette got the SDK's leftover defaults in rows seven and eight — the\n * exact regression that token exists to prevent. `palette.ts` imports nothing,\n * so this pulls no chart library into the slice.\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 = useMemo(\n () => buildBarListRows({ items, sort, max, otherLabel }),\n [items, sort, max, otherLabel],\n );\n const palette = useChartColors();\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: row.color ?? palette[row.index % palette.length],\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":"+MAwEA,SAAgB,EAAQ,CACpB,QACA,iBACA,iBAAiB,GACjB,OAAO,OACP,MACA,aACA,YACA,GAAG,GACU,CACb,IAAM,GAAA,EAAO,EAAA,QAAA,KACH,EAAA,iBAAiB,CAAE,QAAO,OAAM,MAAK,YAAW,CAAC,EACvD,CAAC,EAAO,EAAM,EAAK,CAAU,CACjC,EACM,EAAU,EAAA,eAAe,EAE/B,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,gBAAiB,EAAI,OAAS,EAAQ,EAAI,MAAQ,EAAQ,OAC9D,CACH,CAAA,CACA,CAAA,CACL,CArBK,EAAA,GAAG,EAAI,MAAM,GAAG,EAAI,OAqBzB,CACP,CACD,CAAA,CAEZ"}
@@ -6,7 +6,12 @@ import { useMemo as i } from "react";
6
6
  import { jsx as a, jsxs as o } from "react/jsx-runtime";
7
7
  //#region src/components/BarList/BarList.tsx
8
8
  function s({ items: s, valueFormatter: c, showPercentage: l = !1, sort: u = "desc", max: d, otherLabel: f, className: p, ...m }) {
9
- let h = i(() => n(s, u, d, f), [
9
+ let h = i(() => n({
10
+ items: s,
11
+ sort: u,
12
+ max: d,
13
+ otherLabel: f
14
+ }), [
10
15
  s,
11
16
  u,
12
17
  d,
@@ -1 +1 @@
1
- {"version":3,"file":"BarList.js","names":[],"sources":["../../../src/components/BarList/BarList.tsx"],"sourcesContent":["import { useMemo, type HTMLAttributes } from \"react\";\n\nimport { useChartColors } from \"@/charts/use-chart-colors\";\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 * Colors come from {@link useChartColors}, the same resolver every chart in the\n * SDK uses, so a theme that declares fewer series than the eight\n * `--tempest-chart-*` slots cycles within its own palette. Reading the tokens\n * through CSS `var()` could not do that: the count lives in\n * `--tempest-chart-count`, which CSS cannot use as a modulus, so a six-color\n * brand palette got the SDK's leftover defaults in rows seven and eight — the\n * exact regression that token exists to prevent. `palette.ts` imports nothing,\n * so this pulls no chart library into the slice.\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 = useMemo(\n () => buildBarListRows(items, sort, max, otherLabel),\n [items, sort, max, otherLabel],\n );\n const palette = useChartColors();\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: row.color ?? palette[row.index % palette.length],\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":";;;;;;;AAwEA,SAAgB,EAAQ,EACpB,UACA,mBACA,oBAAiB,IACjB,UAAO,QACP,QACA,eACA,cACA,GAAG,KACU;CACb,IAAM,IAAO,QACH,EAAiB,GAAO,GAAM,GAAK,CAAU,GACnD;EAAC;EAAO;EAAM;EAAK;CAAU,CACjC,GACM,IAAU,EAAe;CAE/B,OACI,kBAAC,MAAD;EAAI,WAAW,EAAG,EAAO,MAAM,CAAS;EAAG,GAAI;EAC1C,UAAA,EAAK,KAAK,MACP,kBAAC,MAAD;GAAsC,WAAW,EAAO;GAAxD,UAAA,CACI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACI,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAQ,UAAA,EAAI;IAAY,CAAA,GAChD,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAxB,UAAA,CACK,IAAiB,EAAe,EAAI,KAAK,IAAI,EAAI,OACjD,KACG,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAxB,UAAA,CACK,KAAK,MAAM,EAAI,UAAU,GAAE,GAC1B;KAER,CAAA,CAAA;IACL,CAAA,CAAA;GACL,CAAA,GAAA,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAO,eAAY;IACtC,UAAA,kBAAC,OAAD;KACI,WAAW,EAAO;KAClB,OAAO;MACH,OAAO,GAAG,EAAI,MAAM;MACpB,iBAAiB,EAAI,SAAS,EAAQ,EAAI,QAAQ,EAAQ;KAC9D;IACH,CAAA;GACA,CAAA,CACL;EArBK,GAAA,GAAG,EAAI,MAAM,GAAG,EAAI,OAqBzB,CACP;CACD,CAAA;AAEZ"}
1
+ {"version":3,"file":"BarList.js","names":[],"sources":["../../../src/components/BarList/BarList.tsx"],"sourcesContent":["import { useMemo, type HTMLAttributes } from \"react\";\n\nimport { useChartColors } from \"@/charts/use-chart-colors\";\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 * Colors come from {@link useChartColors}, the same resolver every chart in the\n * SDK uses, so a theme that declares fewer series than the eight\n * `--tempest-chart-*` slots cycles within its own palette. Reading the tokens\n * through CSS `var()` could not do that: the count lives in\n * `--tempest-chart-count`, which CSS cannot use as a modulus, so a six-color\n * brand palette got the SDK's leftover defaults in rows seven and eight — the\n * exact regression that token exists to prevent. `palette.ts` imports nothing,\n * so this pulls no chart library into the slice.\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 = useMemo(\n () => buildBarListRows({ items, sort, max, otherLabel }),\n [items, sort, max, otherLabel],\n );\n const palette = useChartColors();\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: row.color ?? palette[row.index % palette.length],\n }}\n />\n </div>\n </li>\n ))}\n </ul>\n );\n}\n"],"mappings":";;;;;;;AAwEA,SAAgB,EAAQ,EACpB,UACA,mBACA,oBAAiB,IACjB,UAAO,QACP,QACA,eACA,cACA,GAAG,KACU;CACb,IAAM,IAAO,QACH,EAAiB;EAAE;EAAO;EAAM;EAAK;CAAW,CAAC,GACvD;EAAC;EAAO;EAAM;EAAK;CAAU,CACjC,GACM,IAAU,EAAe;CAE/B,OACI,kBAAC,MAAD;EAAI,WAAW,EAAG,EAAO,MAAM,CAAS;EAAG,GAAI;EAC1C,UAAA,EAAK,KAAK,MACP,kBAAC,MAAD;GAAsC,WAAW,EAAO;GAAxD,UAAA,CACI,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAvB,UAAA,CACI,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAQ,UAAA,EAAI;IAAY,CAAA,GAChD,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAxB,UAAA,CACK,IAAiB,EAAe,EAAI,KAAK,IAAI,EAAI,OACjD,KACG,kBAAC,QAAD;MAAM,WAAW,EAAO;MAAxB,UAAA,CACK,KAAK,MAAM,EAAI,UAAU,GAAE,GAC1B;KAER,CAAA,CAAA;IACL,CAAA,CAAA;GACL,CAAA,GAAA,kBAAC,OAAD;IAAK,WAAW,EAAO;IAAO,eAAY;IACtC,UAAA,kBAAC,OAAD;KACI,WAAW,EAAO;KAClB,OAAO;MACH,OAAO,GAAG,EAAI,MAAM;MACpB,iBAAiB,EAAI,SAAS,EAAQ,EAAI,QAAQ,EAAQ;KAC9D;IACH,CAAA;GACA,CAAA,CACL;EArBK,GAAA,GAAG,EAAI,MAAM,GAAG,EAAI,OAqBzB,CACP;CACD,CAAA;AAEZ"}
@@ -1,2 +1,2 @@
1
- const e=require("../../utils/numbers.cjs");function t(t,n,r,i){let a=t.filter(e=>Number.isFinite(e.value)),o=a.reduce((e,t)=>e+Math.max(t.value,0),0),s=n===`none`?[...a]:[...a].sort((e,t)=>n===`asc`?e.value-t.value:t.value-e.value),c=r!==void 0&&r>0?r:s.length,l=s.slice(0,c),u=i===void 0?[]:s.slice(c),d=i!==void 0&&u.length>1?[...l,{label:i,value:u.reduce((e,t)=>e+t.value,0)}]:[...l,...u],f=d.reduce((e,t)=>Math.max(e,t.value),0);return d.map((t,n)=>({...t,index:n,percentage:e.percentOf(Math.max(t.value,0),o),width:e.percentOf(Math.max(t.value,0),f)}))}exports.buildBarListRows=t;
1
+ const e=require("../../utils/numbers.cjs");function t(t){let{items:n,sort:r=`desc`,max:i,otherLabel:a}=t,o=n.filter(e=>Number.isFinite(e.value)),s=o.reduce((e,t)=>e+Math.max(t.value,0),0),c=r===`none`?[...o]:[...o].sort((e,t)=>r===`asc`?e.value-t.value:t.value-e.value),l=i!==void 0&&i>0?i:c.length,u=c.slice(0,l),d=a===void 0?[]:c.slice(l),f=a!==void 0&&d.length>1?[...u,{label:a,value:d.reduce((e,t)=>e+t.value,0)}]:[...u,...d],p=f.reduce((e,t)=>Math.max(e,t.value),0);return f.map((t,n)=>({...t,index:n,percentage:e.percentOf(Math.max(t.value,0),s),width:e.percentOf(Math.max(t.value,0),p)}))}exports.buildBarListRows=t;
2
2
  //# sourceMappingURL=bar-list-model.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bar-list-model.cjs","names":[],"sources":["../../../src/components/BarList/bar-list-model.ts"],"sourcesContent":["// The arithmetic behind BarList, kept out of the component so the decisions that\n// are easy to get wrong — an empty total, a negative value, what \"100% wide\"\n// means — are testable without rendering anything.\n\nimport { percentOf } from \"@/utils/numbers\";\n\n/** One entry of a {@link BarList}. */\nexport interface BarListItem {\n /** What the row is called. */\n label: string;\n /** The measured amount. */\n value: number;\n /** Override the bar colour. Defaults to the chart series token for its position. */\n color?: string;\n}\n\n/** How the list orders itself before drawing. */\nexport type BarListSort = \"desc\" | \"asc\" | \"none\";\n\n/** One row, with everything the component needs to draw it. */\nexport interface BarListRow extends BarListItem {\n /** Share of the total, 0–100. */\n percentage: number;\n /** Bar width relative to the largest row, 0–100. */\n width: number;\n /** Position in the original palette cycle. */\n index: number;\n}\n\n/**\n * Order, truncate and measure the rows.\n *\n * Two different numbers come out of this, and conflating them is the classic bug\n * in a hand-written bar list:\n *\n * - `percentage` is the row's share of the **total**, which is what the label\n * claims when it reads \"32%\".\n * - `width` is relative to the **largest** row, so the biggest bar fills the\n * track. Scaling width by the total instead leaves every bar short in a list of\n * many small values, and the chart stops being readable exactly when it has the\n * most rows.\n *\n * The total counts positive values only. A negative amount draws no bar (a bar of\n * negative width does not exist) and reports 0%, but its number is still shown —\n * hiding the row would be worse than showing an odd one.\n *\n * @param items - The rows as given.\n * @param sort - Ordering to apply. `\"none\"` keeps the caller's order.\n * @param max - Keep at most this many rows.\n * @param otherLabel - Aggregate what `max` cut into one row with this label.\n * @returns The rows to draw, in order.\n */\nexport function buildBarListRows(\n items: readonly BarListItem[],\n sort: BarListSort,\n max: number | undefined,\n otherLabel: string | undefined,\n): BarListRow[] {\n const usable = items.filter((item) => Number.isFinite(item.value));\n const total = usable.reduce((sum, item) => sum + Math.max(item.value, 0), 0);\n\n const ordered =\n sort === \"none\"\n ? [...usable]\n : [...usable].sort((a, b) => (sort === \"asc\" ? a.value - b.value : b.value - a.value));\n\n const limit = max !== undefined && max > 0 ? max : ordered.length;\n const kept = ordered.slice(0, limit);\n const cut = otherLabel === undefined ? [] : ordered.slice(limit);\n\n const shown: BarListItem[] =\n otherLabel !== undefined && cut.length > 1\n ? [\n ...kept,\n { label: otherLabel, value: cut.reduce((sum, item) => sum + item.value, 0) },\n ]\n : [...kept, ...cut];\n\n const largest = shown.reduce((high, item) => Math.max(high, item.value), 0);\n\n return shown.map((item, index) => ({\n ...item,\n index,\n percentage: percentOf(Math.max(item.value, 0), total),\n width: percentOf(Math.max(item.value, 0), largest),\n }));\n}\n"],"mappings":"2CAoDA,SAAgB,EACZ,EACA,EACA,EACA,EACY,CACZ,IAAM,EAAS,EAAM,OAAQ,GAAS,OAAO,SAAS,EAAK,KAAK,CAAC,EAC3D,EAAQ,EAAO,QAAQ,EAAK,IAAS,EAAM,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAC,EAErE,EACF,IAAS,OACH,CAAC,GAAG,CAAM,EACV,CAAC,GAAG,CAAM,CAAC,CAAC,MAAM,EAAG,IAAO,IAAS,MAAQ,EAAE,MAAQ,EAAE,MAAQ,EAAE,MAAQ,EAAE,KAAM,EAEvF,EAAQ,IAAQ,IAAA,IAAa,EAAM,EAAI,EAAM,EAAQ,OACrD,EAAO,EAAQ,MAAM,EAAG,CAAK,EAC7B,EAAM,IAAe,IAAA,GAAY,CAAC,EAAI,EAAQ,MAAM,CAAK,EAEzD,EACF,IAAe,IAAA,IAAa,EAAI,OAAS,EACnC,CACI,GAAG,EACH,CAAE,MAAO,EAAY,MAAO,EAAI,QAAQ,EAAK,IAAS,EAAM,EAAK,MAAO,CAAC,CAAE,CAC/E,EACA,CAAC,GAAG,EAAM,GAAG,CAAG,EAEpB,EAAU,EAAM,QAAQ,EAAM,IAAS,KAAK,IAAI,EAAM,EAAK,KAAK,EAAG,CAAC,EAE1E,OAAO,EAAM,KAAK,EAAM,KAAW,CAC/B,GAAG,EACH,QACA,WAAY,EAAA,UAAU,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAK,EACpD,MAAO,EAAA,UAAU,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAO,CACrD,EAAE,CACN"}
1
+ {"version":3,"file":"bar-list-model.cjs","names":[],"sources":["../../../src/components/BarList/bar-list-model.ts"],"sourcesContent":["// The arithmetic behind BarList, kept out of the component so the decisions that\n// are easy to get wrong — an empty total, a negative value, what \"100% wide\"\n// means — are testable without rendering anything.\n\nimport { percentOf } from \"@/utils/numbers\";\n\n/** One entry of a {@link BarList}. */\nexport interface BarListItem {\n /** What the row is called. */\n label: string;\n /** The measured amount. */\n value: number;\n /** Override the bar colour. Defaults to the chart series token for its position. */\n color?: string;\n}\n\n/** How the list orders itself before drawing. */\nexport type BarListSort = \"desc\" | \"asc\" | \"none\";\n\n/** One row, with everything the component needs to draw it. */\nexport interface BarListRow extends BarListItem {\n /** Share of the total, 0–100. */\n percentage: number;\n /** Bar width relative to the largest row, 0–100. */\n width: number;\n /** Position in the original palette cycle. */\n index: number;\n}\n\n/** What {@link buildBarListRows} needs to reduce a list into rows. */\nexport interface BuildBarListRowsOptions {\n /** The rows as given. */\n items: readonly BarListItem[];\n /** Ordering to apply. `\"none\"` keeps the caller's order. Default `\"desc\"`. */\n sort?: BarListSort;\n /** Keep at most this many rows. */\n max?: number;\n /** Aggregate what `max` cut into one row with this label. */\n otherLabel?: string;\n}\n\n/**\n * Order, truncate and measure the rows.\n *\n * Two different numbers come out of this, and conflating them is the classic bug\n * in a hand-written bar list:\n *\n * - `percentage` is the row's share of the **total**, which is what the label\n * claims when it reads \"32%\".\n * - `width` is relative to the **largest** row, so the biggest bar fills the\n * track. Scaling width by the total instead leaves every bar short in a list of\n * many small values, and the chart stops being readable exactly when it has the\n * most rows.\n *\n * The total counts positive values only. A negative amount draws no bar (a bar of\n * negative width does not exist) and reports 0%, but its number is still shown —\n * hiding the row would be worse than showing an odd one.\n *\n * @param options - The rows and how to reduce them.\n * @returns The rows to draw, in order.\n */\nexport function buildBarListRows(options: BuildBarListRowsOptions): BarListRow[] {\n const { items, sort = \"desc\", max, otherLabel } = options;\n const usable = items.filter((item) => Number.isFinite(item.value));\n const total = usable.reduce((sum, item) => sum + Math.max(item.value, 0), 0);\n\n const ordered =\n sort === \"none\"\n ? [...usable]\n : [...usable].sort((a, b) => (sort === \"asc\" ? a.value - b.value : b.value - a.value));\n\n const limit = max !== undefined && max > 0 ? max : ordered.length;\n const kept = ordered.slice(0, limit);\n const cut = otherLabel === undefined ? [] : ordered.slice(limit);\n\n const shown: BarListItem[] =\n otherLabel !== undefined && cut.length > 1\n ? [\n ...kept,\n { label: otherLabel, value: cut.reduce((sum, item) => sum + item.value, 0) },\n ]\n : [...kept, ...cut];\n\n const largest = shown.reduce((high, item) => Math.max(high, item.value), 0);\n\n return shown.map((item, index) => ({\n ...item,\n index,\n percentage: percentOf(Math.max(item.value, 0), total),\n width: percentOf(Math.max(item.value, 0), largest),\n }));\n}\n"],"mappings":"2CA6DA,SAAgB,EAAiB,EAAgD,CAC7E,GAAM,CAAE,QAAO,OAAO,OAAQ,MAAK,cAAe,EAC5C,EAAS,EAAM,OAAQ,GAAS,OAAO,SAAS,EAAK,KAAK,CAAC,EAC3D,EAAQ,EAAO,QAAQ,EAAK,IAAS,EAAM,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAC,EAErE,EACF,IAAS,OACH,CAAC,GAAG,CAAM,EACV,CAAC,GAAG,CAAM,CAAC,CAAC,MAAM,EAAG,IAAO,IAAS,MAAQ,EAAE,MAAQ,EAAE,MAAQ,EAAE,MAAQ,EAAE,KAAM,EAEvF,EAAQ,IAAQ,IAAA,IAAa,EAAM,EAAI,EAAM,EAAQ,OACrD,EAAO,EAAQ,MAAM,EAAG,CAAK,EAC7B,EAAM,IAAe,IAAA,GAAY,CAAC,EAAI,EAAQ,MAAM,CAAK,EAEzD,EACF,IAAe,IAAA,IAAa,EAAI,OAAS,EACnC,CACI,GAAG,EACH,CAAE,MAAO,EAAY,MAAO,EAAI,QAAQ,EAAK,IAAS,EAAM,EAAK,MAAO,CAAC,CAAE,CAC/E,EACA,CAAC,GAAG,EAAM,GAAG,CAAG,EAEpB,EAAU,EAAM,QAAQ,EAAM,IAAS,KAAK,IAAI,EAAM,EAAK,KAAK,EAAG,CAAC,EAE1E,OAAO,EAAM,KAAK,EAAM,KAAW,CAC/B,GAAG,EACH,QACA,WAAY,EAAA,UAAU,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAK,EACpD,MAAO,EAAA,UAAU,KAAK,IAAI,EAAK,MAAO,CAAC,EAAG,CAAO,CACrD,EAAE,CACN"}
@@ -1,15 +1,15 @@
1
1
  import { percentOf as e } from "../../utils/numbers.js";
2
2
  //#region src/components/BarList/bar-list-model.ts
3
- function t(t, n, r, i) {
4
- let a = t.filter((e) => Number.isFinite(e.value)), o = a.reduce((e, t) => e + Math.max(t.value, 0), 0), s = n === "none" ? [...a] : [...a].sort((e, t) => n === "asc" ? e.value - t.value : t.value - e.value), c = r !== void 0 && r > 0 ? r : s.length, l = s.slice(0, c), u = i === void 0 ? [] : s.slice(c), d = i !== void 0 && u.length > 1 ? [...l, {
5
- label: i,
6
- value: u.reduce((e, t) => e + t.value, 0)
7
- }] : [...l, ...u], f = d.reduce((e, t) => Math.max(e, t.value), 0);
8
- return d.map((t, n) => ({
3
+ function t(t) {
4
+ let { items: n, sort: r = "desc", max: i, otherLabel: a } = t, o = n.filter((e) => Number.isFinite(e.value)), s = o.reduce((e, t) => e + Math.max(t.value, 0), 0), c = r === "none" ? [...o] : [...o].sort((e, t) => r === "asc" ? e.value - t.value : t.value - e.value), l = i !== void 0 && i > 0 ? i : c.length, u = c.slice(0, l), d = a === void 0 ? [] : c.slice(l), f = a !== void 0 && d.length > 1 ? [...u, {
5
+ label: a,
6
+ value: d.reduce((e, t) => e + t.value, 0)
7
+ }] : [...u, ...d], p = f.reduce((e, t) => Math.max(e, t.value), 0);
8
+ return f.map((t, n) => ({
9
9
  ...t,
10
10
  index: n,
11
- percentage: e(Math.max(t.value, 0), o),
12
- width: e(Math.max(t.value, 0), f)
11
+ percentage: e(Math.max(t.value, 0), s),
12
+ width: e(Math.max(t.value, 0), p)
13
13
  }));
14
14
  }
15
15
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"bar-list-model.js","names":[],"sources":["../../../src/components/BarList/bar-list-model.ts"],"sourcesContent":["// The arithmetic behind BarList, kept out of the component so the decisions that\n// are easy to get wrong — an empty total, a negative value, what \"100% wide\"\n// means — are testable without rendering anything.\n\nimport { percentOf } from \"@/utils/numbers\";\n\n/** One entry of a {@link BarList}. */\nexport interface BarListItem {\n /** What the row is called. */\n label: string;\n /** The measured amount. */\n value: number;\n /** Override the bar colour. Defaults to the chart series token for its position. */\n color?: string;\n}\n\n/** How the list orders itself before drawing. */\nexport type BarListSort = \"desc\" | \"asc\" | \"none\";\n\n/** One row, with everything the component needs to draw it. */\nexport interface BarListRow extends BarListItem {\n /** Share of the total, 0–100. */\n percentage: number;\n /** Bar width relative to the largest row, 0–100. */\n width: number;\n /** Position in the original palette cycle. */\n index: number;\n}\n\n/**\n * Order, truncate and measure the rows.\n *\n * Two different numbers come out of this, and conflating them is the classic bug\n * in a hand-written bar list:\n *\n * - `percentage` is the row's share of the **total**, which is what the label\n * claims when it reads \"32%\".\n * - `width` is relative to the **largest** row, so the biggest bar fills the\n * track. Scaling width by the total instead leaves every bar short in a list of\n * many small values, and the chart stops being readable exactly when it has the\n * most rows.\n *\n * The total counts positive values only. A negative amount draws no bar (a bar of\n * negative width does not exist) and reports 0%, but its number is still shown —\n * hiding the row would be worse than showing an odd one.\n *\n * @param items - The rows as given.\n * @param sort - Ordering to apply. `\"none\"` keeps the caller's order.\n * @param max - Keep at most this many rows.\n * @param otherLabel - Aggregate what `max` cut into one row with this label.\n * @returns The rows to draw, in order.\n */\nexport function buildBarListRows(\n items: readonly BarListItem[],\n sort: BarListSort,\n max: number | undefined,\n otherLabel: string | undefined,\n): BarListRow[] {\n const usable = items.filter((item) => Number.isFinite(item.value));\n const total = usable.reduce((sum, item) => sum + Math.max(item.value, 0), 0);\n\n const ordered =\n sort === \"none\"\n ? [...usable]\n : [...usable].sort((a, b) => (sort === \"asc\" ? a.value - b.value : b.value - a.value));\n\n const limit = max !== undefined && max > 0 ? max : ordered.length;\n const kept = ordered.slice(0, limit);\n const cut = otherLabel === undefined ? [] : ordered.slice(limit);\n\n const shown: BarListItem[] =\n otherLabel !== undefined && cut.length > 1\n ? [\n ...kept,\n { label: otherLabel, value: cut.reduce((sum, item) => sum + item.value, 0) },\n ]\n : [...kept, ...cut];\n\n const largest = shown.reduce((high, item) => Math.max(high, item.value), 0);\n\n return shown.map((item, index) => ({\n ...item,\n index,\n percentage: percentOf(Math.max(item.value, 0), total),\n width: percentOf(Math.max(item.value, 0), largest),\n }));\n}\n"],"mappings":";;AAoDA,SAAgB,EACZ,GACA,GACA,GACA,GACY;CACZ,IAAM,IAAS,EAAM,QAAQ,MAAS,OAAO,SAAS,EAAK,KAAK,CAAC,GAC3D,IAAQ,EAAO,QAAQ,GAAK,MAAS,IAAM,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAC,GAErE,IACF,MAAS,SACH,CAAC,GAAG,CAAM,IACV,CAAC,GAAG,CAAM,CAAC,CAAC,MAAM,GAAG,MAAO,MAAS,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAM,GAEvF,IAAQ,MAAQ,KAAA,KAAa,IAAM,IAAI,IAAM,EAAQ,QACrD,IAAO,EAAQ,MAAM,GAAG,CAAK,GAC7B,IAAM,MAAe,KAAA,IAAY,CAAC,IAAI,EAAQ,MAAM,CAAK,GAEzD,IACF,MAAe,KAAA,KAAa,EAAI,SAAS,IACnC,CACI,GAAG,GACH;EAAE,OAAO;EAAY,OAAO,EAAI,QAAQ,GAAK,MAAS,IAAM,EAAK,OAAO,CAAC;CAAE,CAC/E,IACA,CAAC,GAAG,GAAM,GAAG,CAAG,GAEpB,IAAU,EAAM,QAAQ,GAAM,MAAS,KAAK,IAAI,GAAM,EAAK,KAAK,GAAG,CAAC;CAE1E,OAAO,EAAM,KAAK,GAAM,OAAW;EAC/B,GAAG;EACH;EACA,YAAY,EAAU,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAK;EACpD,OAAO,EAAU,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAO;CACrD,EAAE;AACN"}
1
+ {"version":3,"file":"bar-list-model.js","names":[],"sources":["../../../src/components/BarList/bar-list-model.ts"],"sourcesContent":["// The arithmetic behind BarList, kept out of the component so the decisions that\n// are easy to get wrong — an empty total, a negative value, what \"100% wide\"\n// means — are testable without rendering anything.\n\nimport { percentOf } from \"@/utils/numbers\";\n\n/** One entry of a {@link BarList}. */\nexport interface BarListItem {\n /** What the row is called. */\n label: string;\n /** The measured amount. */\n value: number;\n /** Override the bar colour. Defaults to the chart series token for its position. */\n color?: string;\n}\n\n/** How the list orders itself before drawing. */\nexport type BarListSort = \"desc\" | \"asc\" | \"none\";\n\n/** One row, with everything the component needs to draw it. */\nexport interface BarListRow extends BarListItem {\n /** Share of the total, 0–100. */\n percentage: number;\n /** Bar width relative to the largest row, 0–100. */\n width: number;\n /** Position in the original palette cycle. */\n index: number;\n}\n\n/** What {@link buildBarListRows} needs to reduce a list into rows. */\nexport interface BuildBarListRowsOptions {\n /** The rows as given. */\n items: readonly BarListItem[];\n /** Ordering to apply. `\"none\"` keeps the caller's order. Default `\"desc\"`. */\n sort?: BarListSort;\n /** Keep at most this many rows. */\n max?: number;\n /** Aggregate what `max` cut into one row with this label. */\n otherLabel?: string;\n}\n\n/**\n * Order, truncate and measure the rows.\n *\n * Two different numbers come out of this, and conflating them is the classic bug\n * in a hand-written bar list:\n *\n * - `percentage` is the row's share of the **total**, which is what the label\n * claims when it reads \"32%\".\n * - `width` is relative to the **largest** row, so the biggest bar fills the\n * track. Scaling width by the total instead leaves every bar short in a list of\n * many small values, and the chart stops being readable exactly when it has the\n * most rows.\n *\n * The total counts positive values only. A negative amount draws no bar (a bar of\n * negative width does not exist) and reports 0%, but its number is still shown —\n * hiding the row would be worse than showing an odd one.\n *\n * @param options - The rows and how to reduce them.\n * @returns The rows to draw, in order.\n */\nexport function buildBarListRows(options: BuildBarListRowsOptions): BarListRow[] {\n const { items, sort = \"desc\", max, otherLabel } = options;\n const usable = items.filter((item) => Number.isFinite(item.value));\n const total = usable.reduce((sum, item) => sum + Math.max(item.value, 0), 0);\n\n const ordered =\n sort === \"none\"\n ? [...usable]\n : [...usable].sort((a, b) => (sort === \"asc\" ? a.value - b.value : b.value - a.value));\n\n const limit = max !== undefined && max > 0 ? max : ordered.length;\n const kept = ordered.slice(0, limit);\n const cut = otherLabel === undefined ? [] : ordered.slice(limit);\n\n const shown: BarListItem[] =\n otherLabel !== undefined && cut.length > 1\n ? [\n ...kept,\n { label: otherLabel, value: cut.reduce((sum, item) => sum + item.value, 0) },\n ]\n : [...kept, ...cut];\n\n const largest = shown.reduce((high, item) => Math.max(high, item.value), 0);\n\n return shown.map((item, index) => ({\n ...item,\n index,\n percentage: percentOf(Math.max(item.value, 0), total),\n width: percentOf(Math.max(item.value, 0), largest),\n }));\n}\n"],"mappings":";;AA6DA,SAAgB,EAAiB,GAAgD;CAC7E,IAAM,EAAE,UAAO,UAAO,QAAQ,QAAK,kBAAe,GAC5C,IAAS,EAAM,QAAQ,MAAS,OAAO,SAAS,EAAK,KAAK,CAAC,GAC3D,IAAQ,EAAO,QAAQ,GAAK,MAAS,IAAM,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAC,GAErE,IACF,MAAS,SACH,CAAC,GAAG,CAAM,IACV,CAAC,GAAG,CAAM,CAAC,CAAC,MAAM,GAAG,MAAO,MAAS,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAM,GAEvF,IAAQ,MAAQ,KAAA,KAAa,IAAM,IAAI,IAAM,EAAQ,QACrD,IAAO,EAAQ,MAAM,GAAG,CAAK,GAC7B,IAAM,MAAe,KAAA,IAAY,CAAC,IAAI,EAAQ,MAAM,CAAK,GAEzD,IACF,MAAe,KAAA,KAAa,EAAI,SAAS,IACnC,CACI,GAAG,GACH;EAAE,OAAO;EAAY,OAAO,EAAI,QAAQ,GAAK,MAAS,IAAM,EAAK,OAAO,CAAC;CAAE,CAC/E,IACA,CAAC,GAAG,GAAM,GAAG,CAAG,GAEpB,IAAU,EAAM,QAAQ,GAAM,MAAS,KAAK,IAAI,GAAM,EAAK,KAAK,GAAG,CAAC;CAE1E,OAAO,EAAM,KAAK,GAAM,OAAW;EAC/B,GAAG;EACH;EACA,YAAY,EAAU,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAK;EACpD,OAAO,EAAU,KAAK,IAAI,EAAK,OAAO,CAAC,GAAG,CAAO;CACrD,EAAE;AACN"}
@@ -1,7 +1,7 @@
1
- const e=require("./markdown-url.cjs");var t=/^\s{0,3}(`{3,}|~{3,})\s*([\w+-]*)\s*$/,n=/^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/,r=/^\s{0,3}([-*_])\s*(?:\1\s*){2,}$/,i=/^\s{0,3}>\s?(.*)$/,a=/^(\s*)([-*+])\s+(.*)$/,o=/^(\s*)(\d{1,9})[.)]\s+(.*)$/,s=/^\s{0,3}\|[\s|:-]*$|^\s{0,3}:?-+:?(\s*\|\s*:?-+:?)+\s*\|?\s*$/;function c(e){return l(e.replace(/\r\n?/g,`
1
+ const e=require("./markdown-url.cjs");var t=/^\s{0,3}(`{3,}|~{3,})\s*([\w+-]*)\s*$/,n=/^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/,r=/^\s{0,3}([-*_])\s*(?:\1\s*){2,}$/,i=/^\s{0,3}>\s?(.*)$/,a=/^(\s*)([-*+])\s+(.*)$/,o=/^(\s*)(\d{1,9})[.)]\s+(.*)$/,s=/ {2,}$/,c=/^\s{0,3}\|[\s|:-]*$|^\s{0,3}:?-+:?(\s*\|\s*:?-+:?)+\s*\|?\s*$/;function l(e){return u(e.replace(/\r\n?/g,`
2
2
  `).split(`
3
- `))}function l(e){let c=[],d=0;for(;d<e.length;){let p=e[d];if(p.trim()===``){d+=1;continue}let h=t.exec(p);if(h){let t=h[1][0],n=[];for(d+=1;d<e.length&&!RegExp(`^\\s{0,3}${t}{3,}\\s*$`).test(e[d]);)n.push(e[d]),d+=1;d+=1,c.push({type:`code`,language:h[2]||null,value:n.join(`
4
- `)});continue}let g=n.exec(p);if(g){c.push({type:`heading`,level:g[1].length,children:m(g[2])}),d+=1;continue}if(r.test(p)){c.push({type:`rule`}),d+=1;continue}if(i.test(p)){let t=[];for(;d<e.length;){let n=i.exec(e[d]);if(n){t.push(n[1]),d+=1;continue}if(e[d].trim()===``)break;t.push(e[d]),d+=1}c.push({type:`quote`,children:l(t)});continue}if(a.test(p)||o.test(p)){let[t,n]=u(e,d);c.push(t),d=n;continue}if(d+1<e.length&&p.includes(`|`)&&s.test(e[d+1])){let[t,n]=f(e,d);c.push(t),d=n;continue}let _=[];for(;d<e.length&&e[d].trim()!==``;){let s=e[d];if(n.test(s)||r.test(s)||t.test(s)||i.test(s)||a.test(s)||o.test(s))break;_.push(s.trim()),d+=1}_.length>0&&c.push({type:`paragraph`,children:m(_.join(`
5
- `))})}return c}function u(e,t){let n=a.exec(e[t]),r=o.exec(e[t]),i=r!==null,s=((r??n)?.[1]??``).length,c=i?Number(r?.[2]??1):1,u=[],d=[],f=t,p=()=>{d.length!==0&&(u.push(l(d)),d=[])};for(;f<e.length;){let t=e[f];if(t.trim()===``){if(f+1<e.length&&e[f+1].trim()!==``){d.push(``),f+=1;continue}break}let n=a.exec(t),r=o.exec(t),c=n??r;if(c&&c[1].length<=s){if(!(i?r:n))break;p(),d.push(c[3]),f+=1;continue}if(c){d.push(t.slice(s)),f+=1;continue}if(t.search(/\S/)>s){d.push(t.trim()),f+=1;continue}break}return p(),[{type:`list`,ordered:i,start:c,items:u},f]}function d(e){let t=e.trim().replace(/^\|/,``).replace(/\|$/,``),n=[],r=``;for(let e=0;e<t.length;e+=1){if(t[e]===`\\`&&t[e+1]===`|`){r+=`|`,e+=1;continue}if(t[e]===`|`){n.push(r),r=``;continue}r+=t[e]}return n.push(r),n.map(e=>e.trim())}function f(e,t){let n=d(e[t]),r=d(e[t+1]).map(e=>{let t=e.startsWith(`:`),n=e.endsWith(`:`);return t&&n?`center`:n?`right`:t?`left`:null}),i=[],a=t+2;for(;a<e.length&&e[a].trim()!==``&&e[a].includes(`|`);)i.push(d(e[a]).map(m)),a+=1;return[{type:`table`,align:r,head:n.map(m),rows:i},a]}var p=[{pattern:/^!\[([^\]]*)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?\)/,build:t=>{let n=e.safeImageUrl(t[2]);return n?{type:`image`,src:n,alt:t[1]}:{type:`text`,value:t[1]}}},{pattern:/^\[([^\]]*)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?\)/,build:t=>{let n=e.safeLinkUrl(t[2]),r=m(t[1]);return n?{type:`link`,href:n,children:r}:{type:`text`,value:t[1]}}},{pattern:/^`([^`]+)`/,build:e=>({type:`code`,value:e[1]})},{pattern:/^\*\*([\s\S]+?)\*\*/,build:e=>({type:`strong`,children:m(e[1])})},{pattern:/^__([\s\S]+?)__/,build:e=>({type:`strong`,children:m(e[1])})},{pattern:/^~~([\s\S]+?)~~/,build:e=>({type:`del`,children:m(e[1])})},{pattern:/^\*([^*\n]+)\*/,build:e=>({type:`em`,children:m(e[1])})},{pattern:/^_([^_\n]+)_/,build:e=>({type:`em`,children:m(e[1])})},{pattern:/^<((?:https?:\/\/|mailto:)[^>\s]+)>/,build:t=>{let n=e.safeLinkUrl(t[1]);return n?{type:`link`,href:n,children:[{type:`text`,value:t[1]}]}:{type:`text`,value:t[1]}}},{pattern:/^ {2,}\n/,build:()=>({type:`break`})},{pattern:/^\\\n/,build:()=>({type:`break`})}];function m(e){let t=[],n=``,r=0,i=()=>{n&&t.push({type:`text`,value:n}),n=``};for(;r<e.length;){if(e[r]===`\\`&&r+1<e.length&&e[r+1]!==`
6
- `){n+=e[r+1],r+=2;continue}let a=e.slice(r),o=!1;for(let e of p){let n=e.pattern.exec(a);if(!n)continue;let s=e.build(n);if(s){i(),t.push(s),r+=n[0].length,o=!0;break}}o||(n+=e[r],r+=1)}return i(),t}exports.parseInline=m,exports.parseMarkdown=c;
3
+ `))}function u(e){let l=[],p=0;for(;p<e.length;){let h=e[p];if(h.trim()===``){p+=1;continue}let _=t.exec(h);if(_){let t=_[1][0],n=[];for(p+=1;p<e.length&&!RegExp(`^\\s{0,3}${t}{3,}\\s*$`).test(e[p]);)n.push(e[p]),p+=1;p+=1,l.push({type:`code`,language:_[2]||null,value:n.join(`
4
+ `)});continue}let v=n.exec(h);if(v){l.push({type:`heading`,level:v[1].length,children:g(v[2])}),p+=1;continue}if(r.test(h)){l.push({type:`rule`}),p+=1;continue}if(i.test(h)){let t=[];for(;p<e.length;){let n=i.exec(e[p]);if(n){t.push(n[1]),p+=1;continue}if(e[p].trim()===``)break;t.push(e[p]),p+=1}l.push({type:`quote`,children:u(t)});continue}if(a.test(h)||o.test(h)){let[t,n]=f(e,p);l.push(t),p=n;continue}if(p+1<e.length&&h.includes(`|`)&&c.test(e[p+1])){let[t,n]=m(e,p);l.push(t),p=n;continue}let y=[],b=[];for(;p<e.length&&e[p].trim()!==``;){let c=e[p];if(n.test(c)||r.test(c)||t.test(c)||i.test(c)||a.test(c)||o.test(c))break;y.push(c.trim()),b.push(s.test(c)),p+=1}y.length>0&&l.push({type:`paragraph`,children:g(d(y,b))})}return l}function d(e,t){return e.map((n,r)=>t[r]&&r<e.length-1?`${n} `:n).join(`
5
+ `)}function f(e,t){let n=a.exec(e[t]),r=o.exec(e[t]),i=r!==null,s=((r??n)?.[1]??``).length,c=i?Number(r?.[2]??1):1,l=[],d=[],f=t,p=()=>{d.length!==0&&(l.push(u(d)),d=[])};for(;f<e.length;){let t=e[f];if(t.trim()===``){if(f+1<e.length&&e[f+1].trim()!==``){d.push(``),f+=1;continue}break}let n=a.exec(t),r=o.exec(t),c=n??r;if(c&&c[1].length<=s){if(!(i?r:n))break;p(),d.push(c[3]),f+=1;continue}if(c){d.push(t.slice(s)),f+=1;continue}if(t.search(/\S/)>s){d.push(t.trim()),f+=1;continue}break}return p(),[{type:`list`,ordered:i,start:c,items:l},f]}function p(e){let t=e.trim().replace(/^\|/,``).replace(/\|$/,``),n=[],r=``;for(let e=0;e<t.length;e+=1){if(t[e]===`\\`&&t[e+1]===`|`){r+=`|`,e+=1;continue}if(t[e]===`|`){n.push(r),r=``;continue}r+=t[e]}return n.push(r),n.map(e=>e.trim())}function m(e,t){let n=p(e[t]),r=p(e[t+1]).map(e=>{let t=e.startsWith(`:`),n=e.endsWith(`:`);return t&&n?`center`:n?`right`:t?`left`:null}),i=[],a=t+2;for(;a<e.length&&e[a].trim()!==``&&e[a].includes(`|`);)i.push(p(e[a]).map(g)),a+=1;return[{type:`table`,align:r,head:n.map(g),rows:i},a]}var h=[{pattern:/^!\[([^\]]*)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?\)/,build:t=>{let n=e.safeImageUrl(t[2]);return n?{type:`image`,src:n,alt:t[1]}:{type:`text`,value:t[1]}}},{pattern:/^\[([^\]]*)\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+"[^"]*")?\)/,build:t=>{let n=e.safeLinkUrl(t[2]),r=g(t[1]);return n?{type:`link`,href:n,children:r}:{type:`text`,value:t[1]}}},{pattern:/^`([^`]+)`/,build:e=>({type:`code`,value:e[1]})},{pattern:/^\*\*([\s\S]+?)\*\*/,build:e=>({type:`strong`,children:g(e[1])})},{pattern:/^__([\s\S]+?)__/,build:e=>({type:`strong`,children:g(e[1])})},{pattern:/^~~([\s\S]+?)~~/,build:e=>({type:`del`,children:g(e[1])})},{pattern:/^\*([^*\n]+)\*/,build:e=>({type:`em`,children:g(e[1])})},{pattern:/^_([^_\n]+)_/,build:e=>({type:`em`,children:g(e[1])})},{pattern:/^<((?:https?:\/\/|mailto:)[^>\s]+)>/,build:t=>{let n=e.safeLinkUrl(t[1]);return n?{type:`link`,href:n,children:[{type:`text`,value:t[1]}]}:{type:`text`,value:t[1]}}},{pattern:/^ {2,}\n/,build:()=>({type:`break`})},{pattern:/^\\\n/,build:()=>({type:`break`})}];function g(e){let t=[],n=``,r=0,i=()=>{n&&t.push({type:`text`,value:n}),n=``};for(;r<e.length;){if(e[r]===`\\`&&r+1<e.length&&e[r+1]!==`
6
+ `){n+=e[r+1],r+=2;continue}let a=e.slice(r),o=!1;for(let e of h){let n=e.pattern.exec(a);if(!n)continue;let s=e.build(n);if(s){i(),t.push(s),r+=n[0].length,o=!0;break}}o||(n+=e[r],r+=1)}return i(),t}exports.parseInline=g,exports.parseMarkdown=l;
7
7
  //# sourceMappingURL=markdown-parse.cjs.map