tempest-react-sdk 0.50.0 → 0.51.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 (45) 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/components/BarList/BarList.cjs +1 -1
  6. package/dist/components/BarList/BarList.cjs.map +1 -1
  7. package/dist/components/BarList/BarList.js +6 -1
  8. package/dist/components/BarList/BarList.js.map +1 -1
  9. package/dist/components/BarList/bar-list-model.cjs +1 -1
  10. package/dist/components/BarList/bar-list-model.cjs.map +1 -1
  11. package/dist/components/BarList/bar-list-model.js +8 -8
  12. package/dist/components/BarList/bar-list-model.js.map +1 -1
  13. package/dist/components/Markdown/markdown-parse.cjs +5 -5
  14. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  15. package/dist/components/Markdown/markdown-parse.js +66 -63
  16. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  17. package/dist/components/Sidebar/Sidebar.cjs +1 -1
  18. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  19. package/dist/components/Sidebar/Sidebar.js +25 -45
  20. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  21. package/dist/components/Sidebar/Sidebar.module.cjs.map +1 -1
  22. package/dist/components/Sidebar/Sidebar.module.js.map +1 -1
  23. package/dist/components/Sidebar/SidebarEntry.cjs +2 -0
  24. package/dist/components/Sidebar/SidebarEntry.cjs.map +1 -0
  25. package/dist/components/Sidebar/SidebarEntry.js +40 -0
  26. package/dist/components/Sidebar/SidebarEntry.js.map +1 -0
  27. package/dist/http/api-client.cjs +1 -1
  28. package/dist/http/api-client.cjs.map +1 -1
  29. package/dist/http/api-client.js +84 -71
  30. package/dist/http/api-client.js.map +1 -1
  31. package/dist/http/timeout.cjs +2 -0
  32. package/dist/http/timeout.cjs.map +1 -0
  33. package/dist/http/timeout.js +22 -0
  34. package/dist/http/timeout.js.map +1 -0
  35. package/dist/icons/material-symbols.cjs.map +1 -1
  36. package/dist/icons/material-symbols.js.map +1 -1
  37. package/dist/styles.css +1 -1
  38. package/dist/tempest-react-sdk.d.ts +68 -6
  39. package/dist/utils/csv.cjs.map +1 -1
  40. package/dist/utils/csv.js.map +1 -1
  41. package/dist/vite/tempest-pwa-dev-sw.cjs +1 -1
  42. package/dist/vite/tempest-pwa-dev-sw.cjs.map +1 -1
  43. package/dist/vite/tempest-pwa-dev-sw.js +49 -41
  44. package/dist/vite/tempest-pwa-dev-sw.js.map +1 -1
  45. 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":"AA+CA,SAAS,EAAY,EAAuB,CAExC,OADK,OAAO,SAAS,CAAK,EACnB,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,CAAK,CAAC,EADD,CAExC,CAgDA,SAAS,EAAc,EAAiB,EAAqB,CAEzD,MADI,CAAC,GAAW,kBAAkB,KAAK,CAAG,GAAK,EAAI,WAAW,OAAO,EAAU,EACxE,GAAG,EAAQ,QAAQ,MAAO,EAAE,EAAE,GAAG,EAAI,QAAQ,MAAO,EAAE,GACjE,CAUA,SAAS,EAAa,EAA+C,CACjE,IAAK,IAAM,KAAW,EAAM,SACxB,EAAQ,MAAM,EACd,EAAQ,gBAAgB,KAAK,EAC7B,EAAQ,KAAK,CAErB,CAaA,SAAS,EACL,EACA,EACA,EACI,CACJ,GAAI,EAAQ,MAAQ,EAAY,OAChC,IAAM,EAAS,EAAQ,KAAK,CAAC,CAAC,KAAK,EACnC,GAAI,EAAO,MAAQ,EAAO,QAAU,EAAS,OAC7C,IAAM,EAAU,EAAQ,IAAI,EAAO,KAAK,EACpC,GAAS,EAAa,CAAO,EACjC,EAAQ,OAAO,EAAO,KAAK,CAC/B,CAEA,SAAgB,EAAc,EAA0B,CAAC,EAAY,CACjE,GAAM,CAAE,UAAU,GAAI,SAAS,EAAG,aAAa,IAAO,EAElD,EAAS,EAAY,EAAQ,QAAU,CAAC,EACtC,EAAa,KAAK,IAAI,EAAG,KAAK,MAAM,CAAM,CAAC,EAM3C,EAAU,IAAI,IASd,EAAQ,IAAI,QAiBlB,SAAS,EAAQ,EAA8D,CAC3E,GAAI,OAAO,MAAU,IAAa,OAAO,KAEzC,IAAM,EAAM,EAAc,EAAS,CAAG,EAChC,EAAW,EAAQ,IAAI,CAAG,EAChC,GAAI,EAGA,OAFA,EAAQ,OAAO,CAAG,EAClB,EAAQ,IAAI,EAAK,CAAQ,EAClB,CAAE,MAAO,EAAU,QAAS,EAAM,EAG7C,IAAM,EAAqB,CACvB,SAAU,MAAM,KAAK,CAAE,OAAQ,CAAW,MAAS,CAC/C,IAAM,EAAU,IAAI,MAAM,CAAG,EAE7B,MADA,GAAQ,QAAU,OACX,CACX,CAAC,EACD,KAAM,CACV,EAKA,OAJA,EAAQ,IAAI,EAAK,CAAK,EAEtB,EAAiB,EAAS,EAAK,CAAU,EAElC,CAAE,QAAO,QAAS,EAAK,CAClC,CAEA,MAAO,CACH,KAAK,EAAK,EAAc,CAAC,EAAG,CACxB,IAAM,EAAW,EAAQ,CAAG,EAC5B,GAAI,CAAC,EAAU,OAEf,IAAM,EAAQ,EAAS,MACjB,EAAU,EAAM,SAAS,EAAM,MACrC,EAAM,MAAQ,EAAM,KAAO,GAAK,EAAM,SAAS,OAE/C,IAAM,EAAO,EAAY,EAAY,QAAU,CAAC,EAChD,EAAM,IAAI,EAAS,CAAI,EAEvB,EAAQ,MAAM,EACd,EAAQ,YAAc,EACtB,EAAQ,OAAS,EAAY,EAAO,CAAM,EAC1C,EAAa,KAAK,CAAC,CAAC,UAAY,CAAC,CAAC,CACtC,EAEA,QAAQ,EAAK,CACT,IAAK,IAAM,KAAO,MAAM,QAAQ,CAAG,EAAI,EAAM,CAAC,CAAG,EAAG,CAChD,IAAM,EAAW,EAAQ,CAAG,EACvB,MAAU,QACf,IAAK,IAAM,KAAW,EAAS,MAAM,SAAU,EAAQ,KAAK,CAChE,CACJ,EAEA,UAAU,EAAQ,CACd,EAAS,EAAY,CAAM,EAC3B,IAAK,IAAM,KAAS,EAAQ,OAAO,EAC/B,IAAK,IAAM,KAAW,EAAM,SACpB,EAAQ,SACZ,EAAQ,OAAS,GAAa,EAAM,IAAI,CAAO,GAAK,GAAK,CAAM,EAG3E,EAEA,KAAK,EAAK,CACN,IAAM,EAAU,EACV,CAAC,EAAQ,IAAI,EAAc,EAAS,CAAG,CAAC,CAAC,EACzC,CAAC,GAAG,EAAQ,OAAO,CAAC,EAC1B,IAAK,IAAM,KAAS,EACX,KACL,IAAK,IAAM,KAAW,EAAM,SACxB,EAAQ,MAAM,EACd,EAAQ,YAAc,CAGlC,EAEA,SAAU,CACN,IAAK,IAAM,KAAS,EAAQ,OAAO,EAAG,EAAa,CAAK,EACxD,EAAQ,MAAM,CAClB,CACJ,CACJ"}
@@ -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
- 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
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-parse.cjs","names":[],"sources":["../../../src/components/Markdown/markdown-parse.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a Markdown block parser is a state\n * machine over line kinds: paragraph, heading, fence, list, quote, table and\n * thematic break each need to see the lines around them to know where they end.\n * parseBlocks is that loop, and cutting it into per-kind functions would hand each\n * one the same lookahead cursor.\n */\n// A small Markdown parser for the subset an app actually renders from untrusted\n// text: comments, ticket bodies, release notes, a product description.\n//\n// It produces a node tree, never an HTML string, and the renderer turns that into\n// React elements. No `dangerouslySetInnerHTML` exists anywhere in this component —\n// which is the property that makes rendering somebody else's Markdown safe, and it\n// is structural rather than a promise about escaping.\n//\n// Raw HTML in the input is therefore not \"sanitized\", it is **text**: `<script>` in\n// a comment renders as the four characters somebody typed. That is the whole point,\n// and it is also the documented limit — this is not a CommonMark implementation and\n// does not try to be.\n\nimport { safeImageUrl, safeLinkUrl } from \"./markdown-url\";\n\n/** Inline content. */\nexport type MarkdownInline =\n | { type: \"text\"; value: string }\n | { type: \"strong\"; children: MarkdownInline[] }\n | { type: \"em\"; children: MarkdownInline[] }\n | { type: \"del\"; children: MarkdownInline[] }\n | { type: \"code\"; value: string }\n | { type: \"link\"; href: string; children: MarkdownInline[] }\n | { type: \"image\"; src: string; alt: string }\n | { type: \"break\" };\n\n/** Column alignment of a table, from the delimiter row. */\nexport type MarkdownAlign = \"left\" | \"center\" | \"right\" | null;\n\n/** Block content. */\nexport type MarkdownBlock =\n | { type: \"heading\"; level: number; children: MarkdownInline[] }\n | { type: \"paragraph\"; children: MarkdownInline[] }\n | { type: \"code\"; language: string | null; value: string }\n | { type: \"quote\"; children: MarkdownBlock[] }\n | { type: \"list\"; ordered: boolean; start: number; items: MarkdownBlock[][] }\n | {\n type: \"table\";\n align: MarkdownAlign[];\n head: MarkdownInline[][];\n rows: MarkdownInline[][][];\n }\n | { type: \"rule\" };\n\nconst FENCE = /^\\s{0,3}(`{3,}|~{3,})\\s*([\\w+-]*)\\s*$/;\nconst HEADING = /^\\s{0,3}(#{1,6})\\s+(.*?)\\s*#*\\s*$/;\nconst RULE = /^\\s{0,3}([-*_])\\s*(?:\\1\\s*){2,}$/;\nconst QUOTE = /^\\s{0,3}>\\s?(.*)$/;\nconst BULLET = /^(\\s*)([-*+])\\s+(.*)$/;\nconst ORDERED = /^(\\s*)(\\d{1,9})[.)]\\s+(.*)$/;\n/**\n * A table's delimiter row.\n *\n * The `|` is required somewhere in the line, and that is what tells a one-column\n * table (`| --- |`) apart from a thematic break (`---`) — a single column is a\n * legitimate table, and the first version of this regex demanded two.\n */\nconst TABLE_DELIMITER = /^\\s{0,3}\\|[\\s|:-]*$|^\\s{0,3}:?-+:?(\\s*\\|\\s*:?-+:?)+\\s*\\|?\\s*$/;\n\n/**\n * Parse a Markdown document into blocks.\n *\n * @param source - Raw Markdown.\n * @returns Block nodes in document order.\n */\nexport function parseMarkdown(source: string): MarkdownBlock[] {\n const lines = source.replace(/\\r\\n?/g, \"\\n\").split(\"\\n\");\n return parseBlocks(lines);\n}\n\n/** Parse a run of lines into blocks. */\nfunction parseBlocks(lines: string[]): MarkdownBlock[] {\n const blocks: MarkdownBlock[] = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n if (line.trim() === \"\") {\n i += 1;\n continue;\n }\n\n const fence = FENCE.exec(line);\n if (fence) {\n const marker = fence[1][0];\n const body: string[] = [];\n i += 1;\n while (i < lines.length && !new RegExp(`^\\\\s{0,3}${marker}{3,}\\\\s*$`).test(lines[i])) {\n body.push(lines[i]);\n i += 1;\n }\n // An unterminated fence still yields a code block: the alternative is to\n // render the rest of the document as prose full of backticks.\n i += 1;\n blocks.push({ type: \"code\", language: fence[2] || null, value: body.join(\"\\n\") });\n continue;\n }\n\n const heading = HEADING.exec(line);\n if (heading) {\n blocks.push({\n type: \"heading\",\n level: heading[1].length,\n children: parseInline(heading[2]),\n });\n i += 1;\n continue;\n }\n\n if (RULE.test(line)) {\n blocks.push({ type: \"rule\" });\n i += 1;\n continue;\n }\n\n if (QUOTE.test(line)) {\n const inner: string[] = [];\n while (i < lines.length) {\n const match = QUOTE.exec(lines[i]);\n if (match) {\n inner.push(match[1]);\n i += 1;\n continue;\n }\n // A blank line ends the quote; a plain line continues it (lazy\n // continuation), which is how people actually write quotes.\n if (lines[i].trim() === \"\") break;\n inner.push(lines[i]);\n i += 1;\n }\n blocks.push({ type: \"quote\", children: parseBlocks(inner) });\n continue;\n }\n\n if (BULLET.test(line) || ORDERED.test(line)) {\n const [list, next] = parseList(lines, i);\n blocks.push(list);\n i = next;\n continue;\n }\n\n if (i + 1 < lines.length && line.includes(\"|\") && TABLE_DELIMITER.test(lines[i + 1])) {\n const [table, next] = parseTable(lines, i);\n blocks.push(table);\n i = next;\n continue;\n }\n\n const paragraph: string[] = [];\n while (i < lines.length && lines[i].trim() !== \"\") {\n const current = lines[i];\n if (\n HEADING.test(current) ||\n RULE.test(current) ||\n FENCE.test(current) ||\n QUOTE.test(current) ||\n BULLET.test(current) ||\n ORDERED.test(current)\n ) {\n break;\n }\n paragraph.push(current.trim());\n i += 1;\n }\n if (paragraph.length > 0) {\n blocks.push({ type: \"paragraph\", children: parseInline(paragraph.join(\"\\n\")) });\n }\n }\n\n return blocks;\n}\n\n/**\n * Parse a list, including nested lists and multi-line items.\n *\n * Nesting is decided by indentation relative to the **first** item's marker, so a\n * list indented inside a quote or another list still reads correctly.\n *\n * @returns The list node and the index of the first line after it.\n */\nfunction parseList(lines: string[], start: number): [MarkdownBlock, number] {\n const bulletStart = BULLET.exec(lines[start]);\n const orderedStart = ORDERED.exec(lines[start]);\n // The two markers are mutually exclusive — `-*+` versus digits — so whichever\n // matched decides the list kind.\n const ordered = orderedStart !== null;\n const first = orderedStart ?? bulletStart;\n const baseIndent = (first?.[1] ?? \"\").length;\n const startNumber = ordered ? Number(orderedStart?.[2] ?? 1) : 1;\n\n const items: MarkdownBlock[][] = [];\n let buffer: string[] = [];\n let i = start;\n\n const flush = (): void => {\n if (buffer.length === 0) return;\n items.push(parseBlocks(buffer));\n buffer = [];\n };\n\n while (i < lines.length) {\n const line = lines[i];\n if (line.trim() === \"\") {\n // A single blank line inside a list is a loose item, not the end. Two in\n // a row end it, which is what a blank-then-paragraph document means.\n if (i + 1 < lines.length && lines[i + 1].trim() !== \"\") {\n buffer.push(\"\");\n i += 1;\n continue;\n }\n break;\n }\n const bullet = BULLET.exec(line);\n const numbered = ORDERED.exec(line);\n const match = bullet ?? numbered;\n\n if (match && match[1].length <= baseIndent) {\n const sameKind = ordered ? Boolean(numbered) : Boolean(bullet);\n if (!sameKind) break;\n flush();\n buffer.push(match[3]);\n i += 1;\n continue;\n }\n if (match) {\n // Deeper marker: keep the indentation so the recursive call sees a list.\n buffer.push(line.slice(baseIndent));\n i += 1;\n continue;\n }\n if (line.search(/\\S/) > baseIndent) {\n buffer.push(line.trim());\n i += 1;\n continue;\n }\n break;\n }\n flush();\n\n return [{ type: \"list\", ordered, start: startNumber, items }, i];\n}\n\n/** Split a table row on unescaped pipes. */\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim().replace(/^\\|/, \"\").replace(/\\|$/, \"\");\n const cells: string[] = [];\n let current = \"\";\n for (let i = 0; i < trimmed.length; i += 1) {\n if (trimmed[i] === \"\\\\\" && trimmed[i + 1] === \"|\") {\n current += \"|\";\n i += 1;\n continue;\n }\n if (trimmed[i] === \"|\") {\n cells.push(current);\n current = \"\";\n continue;\n }\n current += trimmed[i];\n }\n cells.push(current);\n return cells.map((cell) => cell.trim());\n}\n\n/**\n * Parse a GFM pipe table.\n *\n * @returns The table node and the index of the first line after it.\n */\nfunction parseTable(lines: string[], start: number): [MarkdownBlock, number] {\n const head = splitRow(lines[start]);\n const align: MarkdownAlign[] = splitRow(lines[start + 1]).map((cell) => {\n const left = cell.startsWith(\":\");\n const right = cell.endsWith(\":\");\n if (left && right) return \"center\";\n if (right) return \"right\";\n if (left) return \"left\";\n return null;\n });\n\n const rows: MarkdownInline[][][] = [];\n let i = start + 2;\n while (i < lines.length && lines[i].trim() !== \"\" && lines[i].includes(\"|\")) {\n rows.push(splitRow(lines[i]).map(parseInline));\n i += 1;\n }\n\n return [{ type: \"table\", align, head: head.map(parseInline), rows }, i];\n}\n\n/** Inline delimiters, longest marker first so `**` wins over `*`. */\nconst INLINE_RULES: Array<{\n pattern: RegExp;\n build: (match: RegExpExecArray) => MarkdownInline | null;\n}> = [\n {\n pattern: /^!\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const src = safeImageUrl(match[2]);\n return src ? { type: \"image\", src, alt: match[1] } : { type: \"text\", value: match[1] };\n },\n },\n {\n pattern: /^\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const href = safeLinkUrl(match[2]);\n const children = parseInline(match[1]);\n // A rejected URL keeps the label as plain text: dropping the text too\n // would silently delete words somebody wrote.\n return href ? { type: \"link\", href, children } : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^`([^`]+)`/, build: (match) => ({ type: \"code\", value: match[1] }) },\n {\n pattern: /^\\*\\*([\\s\\S]+?)\\*\\*/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^__([\\s\\S]+?)__/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^~~([\\s\\S]+?)~~/,\n build: (match) => ({ type: \"del\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^\\*([^*\\n]+)\\*/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^_([^_\\n]+)_/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^<((?:https?:\\/\\/|mailto:)[^>\\s]+)>/,\n build: (match) => {\n const href = safeLinkUrl(match[1]);\n return href\n ? { type: \"link\", href, children: [{ type: \"text\", value: match[1] }] }\n : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^ {2,}\\n/, build: () => ({ type: \"break\" }) },\n { pattern: /^\\\\\\n/, build: () => ({ type: \"break\" }) },\n];\n\n/**\n * Parse inline Markdown.\n *\n * A backslash escapes the next character, so `\\*not italic\\*` renders with the\n * asterisks. Anything that matches no rule is text — an unclosed `**` is two\n * asterisks, not a bold run to the end of the paragraph.\n *\n * @param source - One block's text.\n * @returns Inline nodes.\n */\nexport function parseInline(source: string): MarkdownInline[] {\n const nodes: MarkdownInline[] = [];\n let text = \"\";\n let i = 0;\n\n const flush = (): void => {\n if (text) nodes.push({ type: \"text\", value: text });\n text = \"\";\n };\n\n while (i < source.length) {\n if (source[i] === \"\\\\\" && i + 1 < source.length && source[i + 1] !== \"\\n\") {\n text += source[i + 1];\n i += 2;\n continue;\n }\n\n const rest = source.slice(i);\n let matched = false;\n for (const rule of INLINE_RULES) {\n const match = rule.pattern.exec(rest);\n if (!match) continue;\n const node = rule.build(match);\n if (!node) continue;\n flush();\n nodes.push(node);\n i += match[0].length;\n matched = true;\n break;\n }\n if (matched) continue;\n\n text += source[i];\n i += 1;\n }\n\n flush();\n return nodes;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"markdown-parse.cjs","names":[],"sources":["../../../src/components/Markdown/markdown-parse.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a Markdown block parser is a state\n * machine over line kinds: paragraph, heading, fence, list, quote, table and\n * thematic break each need to see the lines around them to know where they end.\n * parseBlocks is that loop, and cutting it into per-kind functions would hand each\n * one the same lookahead cursor.\n */\n// A small Markdown parser for the subset an app actually renders from untrusted\n// text: comments, ticket bodies, release notes, a product description.\n//\n// It produces a node tree, never an HTML string, and the renderer turns that into\n// React elements. No `dangerouslySetInnerHTML` exists anywhere in this component —\n// which is the property that makes rendering somebody else's Markdown safe, and it\n// is structural rather than a promise about escaping.\n//\n// Raw HTML in the input is therefore not \"sanitized\", it is **text**: `<script>` in\n// a comment renders as the four characters somebody typed. That is the whole point,\n// and it is also the documented limit — this is not a CommonMark implementation and\n// does not try to be.\n\nimport { safeImageUrl, safeLinkUrl } from \"./markdown-url\";\n\n/** Inline content. */\nexport type MarkdownInline =\n | { type: \"text\"; value: string }\n | { type: \"strong\"; children: MarkdownInline[] }\n | { type: \"em\"; children: MarkdownInline[] }\n | { type: \"del\"; children: MarkdownInline[] }\n | { type: \"code\"; value: string }\n | { type: \"link\"; href: string; children: MarkdownInline[] }\n | { type: \"image\"; src: string; alt: string }\n | { type: \"break\" };\n\n/** Column alignment of a table, from the delimiter row. */\nexport type MarkdownAlign = \"left\" | \"center\" | \"right\" | null;\n\n/** Block content. */\nexport type MarkdownBlock =\n | { type: \"heading\"; level: number; children: MarkdownInline[] }\n | { type: \"paragraph\"; children: MarkdownInline[] }\n | { type: \"code\"; language: string | null; value: string }\n | { type: \"quote\"; children: MarkdownBlock[] }\n | { type: \"list\"; ordered: boolean; start: number; items: MarkdownBlock[][] }\n | {\n type: \"table\";\n align: MarkdownAlign[];\n head: MarkdownInline[][];\n rows: MarkdownInline[][][];\n }\n | { type: \"rule\" };\n\nconst FENCE = /^\\s{0,3}(`{3,}|~{3,})\\s*([\\w+-]*)\\s*$/;\nconst HEADING = /^\\s{0,3}(#{1,6})\\s+(.*?)\\s*#*\\s*$/;\nconst RULE = /^\\s{0,3}([-*_])\\s*(?:\\1\\s*){2,}$/;\nconst QUOTE = /^\\s{0,3}>\\s?(.*)$/;\nconst BULLET = /^(\\s*)([-*+])\\s+(.*)$/;\nconst ORDERED = /^(\\s*)(\\d{1,9})[.)]\\s+(.*)$/;\nconst HARD_BREAK = / {2,}$/;\n/**\n * A table's delimiter row.\n *\n * The `|` is required somewhere in the line, and that is what tells a one-column\n * table (`| --- |`) apart from a thematic break (`---`) — a single column is a\n * legitimate table, and the first version of this regex demanded two.\n */\nconst TABLE_DELIMITER = /^\\s{0,3}\\|[\\s|:-]*$|^\\s{0,3}:?-+:?(\\s*\\|\\s*:?-+:?)+\\s*\\|?\\s*$/;\n\n/**\n * Parse a Markdown document into blocks.\n *\n * @param source - Raw Markdown.\n * @returns Block nodes in document order.\n */\nexport function parseMarkdown(source: string): MarkdownBlock[] {\n const lines = source.replace(/\\r\\n?/g, \"\\n\").split(\"\\n\");\n return parseBlocks(lines);\n}\n\n/** Parse a run of lines into blocks. */\nfunction parseBlocks(lines: string[]): MarkdownBlock[] {\n const blocks: MarkdownBlock[] = [];\n let i = 0;\n\n while (i < lines.length) {\n const line = lines[i];\n\n if (line.trim() === \"\") {\n i += 1;\n continue;\n }\n\n const fence = FENCE.exec(line);\n if (fence) {\n const marker = fence[1][0];\n const body: string[] = [];\n i += 1;\n while (i < lines.length && !new RegExp(`^\\\\s{0,3}${marker}{3,}\\\\s*$`).test(lines[i])) {\n body.push(lines[i]);\n i += 1;\n }\n // An unterminated fence still yields a code block: the alternative is to\n // render the rest of the document as prose full of backticks.\n i += 1;\n blocks.push({ type: \"code\", language: fence[2] || null, value: body.join(\"\\n\") });\n continue;\n }\n\n const heading = HEADING.exec(line);\n if (heading) {\n blocks.push({\n type: \"heading\",\n level: heading[1].length,\n children: parseInline(heading[2]),\n });\n i += 1;\n continue;\n }\n\n if (RULE.test(line)) {\n blocks.push({ type: \"rule\" });\n i += 1;\n continue;\n }\n\n if (QUOTE.test(line)) {\n const inner: string[] = [];\n while (i < lines.length) {\n const match = QUOTE.exec(lines[i]);\n if (match) {\n inner.push(match[1]);\n i += 1;\n continue;\n }\n // A blank line ends the quote; a plain line continues it (lazy\n // continuation), which is how people actually write quotes.\n if (lines[i].trim() === \"\") break;\n inner.push(lines[i]);\n i += 1;\n }\n blocks.push({ type: \"quote\", children: parseBlocks(inner) });\n continue;\n }\n\n if (BULLET.test(line) || ORDERED.test(line)) {\n const [list, next] = parseList(lines, i);\n blocks.push(list);\n i = next;\n continue;\n }\n\n if (i + 1 < lines.length && line.includes(\"|\") && TABLE_DELIMITER.test(lines[i + 1])) {\n const [table, next] = parseTable(lines, i);\n blocks.push(table);\n i = next;\n continue;\n }\n\n const paragraph: string[] = [];\n const hardBreaks: boolean[] = [];\n while (i < lines.length && lines[i].trim() !== \"\") {\n const current = lines[i];\n if (\n HEADING.test(current) ||\n RULE.test(current) ||\n FENCE.test(current) ||\n QUOTE.test(current) ||\n BULLET.test(current) ||\n ORDERED.test(current)\n ) {\n break;\n }\n paragraph.push(current.trim());\n hardBreaks.push(HARD_BREAK.test(current));\n i += 1;\n }\n if (paragraph.length > 0) {\n blocks.push({\n type: \"paragraph\",\n children: parseInline(joinParagraph(paragraph, hardBreaks)),\n });\n }\n }\n\n return blocks;\n}\n\n/**\n * Re-join the lines of a paragraph, keeping the two-space hard break.\n *\n * Every line is trimmed before it is parsed, because leading indentation is not\n * content and a trailing space is invisible noise in almost every line. The one\n * exception is the CommonMark hard break: two or more trailing spaces mean\n * \"break the line here\", so trimming first would delete the only mark that says\n * so and the ` \\n` inline rule could never match. The flag is recorded before\n * the trim and the marker put back here, on every line but the last — a break\n * after the final line of a paragraph would break into nothing.\n *\n * @param lines - The trimmed lines, in order.\n * @param hardBreaks - Whether each line ended in two or more spaces.\n * @returns The paragraph source the inline parser reads.\n */\nfunction joinParagraph(lines: readonly string[], hardBreaks: readonly boolean[]): string {\n return lines\n .map((line, index) => (hardBreaks[index] && index < lines.length - 1 ? `${line} ` : line))\n .join(\"\\n\");\n}\n\n/**\n * Parse a list, including nested lists and multi-line items.\n *\n * Nesting is decided by indentation relative to the **first** item's marker, so a\n * list indented inside a quote or another list still reads correctly.\n *\n * @returns The list node and the index of the first line after it.\n */\nfunction parseList(lines: string[], start: number): [MarkdownBlock, number] {\n const bulletStart = BULLET.exec(lines[start]);\n const orderedStart = ORDERED.exec(lines[start]);\n // The two markers are mutually exclusive — `-*+` versus digits — so whichever\n // matched decides the list kind.\n const ordered = orderedStart !== null;\n const first = orderedStart ?? bulletStart;\n const baseIndent = (first?.[1] ?? \"\").length;\n const startNumber = ordered ? Number(orderedStart?.[2] ?? 1) : 1;\n\n const items: MarkdownBlock[][] = [];\n let buffer: string[] = [];\n let i = start;\n\n const flush = (): void => {\n if (buffer.length === 0) return;\n items.push(parseBlocks(buffer));\n buffer = [];\n };\n\n while (i < lines.length) {\n const line = lines[i];\n if (line.trim() === \"\") {\n // A single blank line inside a list is a loose item, not the end. Two in\n // a row end it, which is what a blank-then-paragraph document means.\n if (i + 1 < lines.length && lines[i + 1].trim() !== \"\") {\n buffer.push(\"\");\n i += 1;\n continue;\n }\n break;\n }\n const bullet = BULLET.exec(line);\n const numbered = ORDERED.exec(line);\n const match = bullet ?? numbered;\n\n if (match && match[1].length <= baseIndent) {\n const sameKind = ordered ? Boolean(numbered) : Boolean(bullet);\n if (!sameKind) break;\n flush();\n buffer.push(match[3]);\n i += 1;\n continue;\n }\n if (match) {\n // Deeper marker: keep the indentation so the recursive call sees a list.\n buffer.push(line.slice(baseIndent));\n i += 1;\n continue;\n }\n if (line.search(/\\S/) > baseIndent) {\n buffer.push(line.trim());\n i += 1;\n continue;\n }\n break;\n }\n flush();\n\n return [{ type: \"list\", ordered, start: startNumber, items }, i];\n}\n\n/** Split a table row on unescaped pipes. */\nfunction splitRow(line: string): string[] {\n const trimmed = line.trim().replace(/^\\|/, \"\").replace(/\\|$/, \"\");\n const cells: string[] = [];\n let current = \"\";\n for (let i = 0; i < trimmed.length; i += 1) {\n if (trimmed[i] === \"\\\\\" && trimmed[i + 1] === \"|\") {\n current += \"|\";\n i += 1;\n continue;\n }\n if (trimmed[i] === \"|\") {\n cells.push(current);\n current = \"\";\n continue;\n }\n current += trimmed[i];\n }\n cells.push(current);\n return cells.map((cell) => cell.trim());\n}\n\n/**\n * Parse a GFM pipe table.\n *\n * @returns The table node and the index of the first line after it.\n */\nfunction parseTable(lines: string[], start: number): [MarkdownBlock, number] {\n const head = splitRow(lines[start]);\n const align: MarkdownAlign[] = splitRow(lines[start + 1]).map((cell) => {\n const left = cell.startsWith(\":\");\n const right = cell.endsWith(\":\");\n if (left && right) return \"center\";\n if (right) return \"right\";\n if (left) return \"left\";\n return null;\n });\n\n const rows: MarkdownInline[][][] = [];\n let i = start + 2;\n while (i < lines.length && lines[i].trim() !== \"\" && lines[i].includes(\"|\")) {\n rows.push(splitRow(lines[i]).map(parseInline));\n i += 1;\n }\n\n return [{ type: \"table\", align, head: head.map(parseInline), rows }, i];\n}\n\n/** Inline delimiters, longest marker first so `**` wins over `*`. */\nconst INLINE_RULES: Array<{\n pattern: RegExp;\n build: (match: RegExpExecArray) => MarkdownInline | null;\n}> = [\n {\n pattern: /^!\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const src = safeImageUrl(match[2]);\n return src ? { type: \"image\", src, alt: match[1] } : { type: \"text\", value: match[1] };\n },\n },\n {\n pattern: /^\\[([^\\]]*)\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+\"[^\"]*\")?\\)/,\n build: (match) => {\n const href = safeLinkUrl(match[2]);\n const children = parseInline(match[1]);\n // A rejected URL keeps the label as plain text: dropping the text too\n // would silently delete words somebody wrote.\n return href ? { type: \"link\", href, children } : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^`([^`]+)`/, build: (match) => ({ type: \"code\", value: match[1] }) },\n {\n pattern: /^\\*\\*([\\s\\S]+?)\\*\\*/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^__([\\s\\S]+?)__/,\n build: (match) => ({ type: \"strong\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^~~([\\s\\S]+?)~~/,\n build: (match) => ({ type: \"del\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^\\*([^*\\n]+)\\*/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^_([^_\\n]+)_/,\n build: (match) => ({ type: \"em\", children: parseInline(match[1]) }),\n },\n {\n pattern: /^<((?:https?:\\/\\/|mailto:)[^>\\s]+)>/,\n build: (match) => {\n const href = safeLinkUrl(match[1]);\n return href\n ? { type: \"link\", href, children: [{ type: \"text\", value: match[1] }] }\n : { type: \"text\", value: match[1] };\n },\n },\n { pattern: /^ {2,}\\n/, build: () => ({ type: \"break\" }) },\n { pattern: /^\\\\\\n/, build: () => ({ type: \"break\" }) },\n];\n\n/**\n * Parse inline Markdown.\n *\n * A backslash escapes the next character, so `\\*not italic\\*` renders with the\n * asterisks. Anything that matches no rule is text — an unclosed `**` is two\n * asterisks, not a bold run to the end of the paragraph.\n *\n * @param source - One block's text.\n * @returns Inline nodes.\n */\nexport function parseInline(source: string): MarkdownInline[] {\n const nodes: MarkdownInline[] = [];\n let text = \"\";\n let i = 0;\n\n const flush = (): void => {\n if (text) nodes.push({ type: \"text\", value: text });\n text = \"\";\n };\n\n while (i < source.length) {\n if (source[i] === \"\\\\\" && i + 1 < source.length && source[i + 1] !== \"\\n\") {\n text += source[i + 1];\n i += 2;\n continue;\n }\n\n const rest = source.slice(i);\n let matched = false;\n for (const rule of INLINE_RULES) {\n const match = rule.pattern.exec(rest);\n if (!match) continue;\n const node = rule.build(match);\n if (!node) continue;\n flush();\n nodes.push(node);\n i += match[0].length;\n matched = true;\n break;\n }\n if (matched) continue;\n\n text += source[i];\n i += 1;\n }\n\n flush();\n return nodes;\n}\n"],"mappings":"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"}