@burojs/ui-shadcn 0.5.0 → 0.7.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 (172) hide show
  1. package/README.md +9 -0
  2. package/dist/bulk-summary-BWSAS7k2.js +143 -0
  3. package/dist/bulk-summary-BWSAS7k2.js.map +1 -0
  4. package/dist/bulk-summary-CrEij5vq.cjs +143 -0
  5. package/dist/bulk-summary-CrEij5vq.cjs.map +1 -0
  6. package/dist/candidates-relation-picker-CS_JZ4Fd.js +31 -0
  7. package/dist/candidates-relation-picker-CS_JZ4Fd.js.map +1 -0
  8. package/dist/candidates-relation-picker-y5M6_oe4.cjs +31 -0
  9. package/dist/candidates-relation-picker-y5M6_oe4.cjs.map +1 -0
  10. package/dist/{check-confirm-dialog-CkaVTPT9.js → check-confirm-dialog-2j0gOd7x.js} +392 -103
  11. package/dist/check-confirm-dialog-2j0gOd7x.js.map +1 -0
  12. package/dist/{check-confirm-dialog-BRByTFpg.cjs → check-confirm-dialog-C-NHgMST.cjs} +476 -102
  13. package/dist/check-confirm-dialog-C-NHgMST.cjs.map +1 -0
  14. package/dist/{dialog-B_rt9S1c.js → dialog-C123sVQ8.js} +4 -4
  15. package/dist/dialog-C123sVQ8.js.map +1 -0
  16. package/dist/{dialog-DRNUq9EA.cjs → dialog-vmMdfUWs.cjs} +3 -3
  17. package/dist/dialog-vmMdfUWs.cjs.map +1 -0
  18. package/dist/{filter-builder-B3mZFL-K.cjs → filter-builder-DFu0JP-1.cjs} +959 -128
  19. package/dist/filter-builder-DFu0JP-1.cjs.map +1 -0
  20. package/dist/{filter-builder-DOnaXNfP.js → filter-builder-DaWHUZuV.js} +863 -116
  21. package/dist/filter-builder-DaWHUZuV.js.map +1 -0
  22. package/dist/{flow-runtime-B6zOHYqI.cjs → flow-runtime-BpmrIUb6.cjs} +61 -29
  23. package/dist/flow-runtime-BpmrIUb6.cjs.map +1 -0
  24. package/dist/{flow-runtime-D8h99FSg.js → flow-runtime-DyqTmK3H.js} +62 -30
  25. package/dist/flow-runtime-DyqTmK3H.js.map +1 -0
  26. package/dist/{flow-screen-CX05b3G9.cjs → flow-screen-Bcz9NF2V.cjs} +132 -88
  27. package/dist/flow-screen-Bcz9NF2V.cjs.map +1 -0
  28. package/dist/{flow-screen-CmbARSqg.js → flow-screen-DCdli6Ha.js} +132 -88
  29. package/dist/flow-screen-DCdli6Ha.js.map +1 -0
  30. package/dist/index.cjs +951 -605
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +618 -332
  33. package/dist/index.d.ts +618 -332
  34. package/dist/index.js +863 -522
  35. package/dist/index.js.map +1 -1
  36. package/dist/{inline-flow-host-B7U1OJUe.cjs → inline-flow-host-3vw7LzzD.cjs} +5 -5
  37. package/dist/inline-flow-host-3vw7LzzD.cjs.map +1 -0
  38. package/dist/{inline-flow-host-C3H2UpSA.js → inline-flow-host-C0rI9B7f.js} +5 -5
  39. package/dist/inline-flow-host-C0rI9B7f.js.map +1 -0
  40. package/dist/label-66W1qbke.js.map +1 -1
  41. package/dist/label-w6JYf2cF.cjs.map +1 -1
  42. package/dist/lazy-relation-picker-dialog-Bt8lrEIu.cjs +153 -0
  43. package/dist/lazy-relation-picker-dialog-Bt8lrEIu.cjs.map +1 -0
  44. package/dist/lazy-relation-picker-dialog-CAScLPb2.js +135 -0
  45. package/dist/lazy-relation-picker-dialog-CAScLPb2.js.map +1 -0
  46. package/dist/markdown/index.d.cts +2 -3
  47. package/dist/markdown/index.d.ts +2 -3
  48. package/dist/{page-flow-route-CyXSvXSE.js → page-flow-route-BHUEpn0I.js} +36 -23
  49. package/dist/page-flow-route-BHUEpn0I.js.map +1 -0
  50. package/dist/{page-flow-route-CQ7HsbBm.cjs → page-flow-route-DS8IVTVH.cjs} +41 -28
  51. package/dist/page-flow-route-DS8IVTVH.cjs.map +1 -0
  52. package/dist/{place-node-SIp4XYQH.cjs → place-node-CNb3w-c7.cjs} +2227 -999
  53. package/dist/place-node-CNb3w-c7.cjs.map +1 -0
  54. package/dist/{place-node--GdsP688.js → place-node-CfzQvh4Y.js} +2164 -984
  55. package/dist/place-node-CfzQvh4Y.js.map +1 -0
  56. package/dist/{related-list-cards-B6x9HQQp.js → related-list-cards-BIqqY69Y.js} +9 -4
  57. package/dist/{related-list-cards-B6x9HQQp.js.map → related-list-cards-BIqqY69Y.js.map} +1 -1
  58. package/dist/{related-list-cards-nxuODlIA.cjs → related-list-cards-CelMxl4q.cjs} +9 -4
  59. package/dist/{related-list-cards-nxuODlIA.cjs.map → related-list-cards-CelMxl4q.cjs.map} +1 -1
  60. package/dist/{related-list-timeline-DEdfd-rr.js → related-list-timeline-CRh7xwLD.js} +6 -7
  61. package/dist/related-list-timeline-CRh7xwLD.js.map +1 -0
  62. package/dist/{related-list-timeline-BYJKEbmA.cjs → related-list-timeline-DsLdi8sv.cjs} +5 -6
  63. package/dist/related-list-timeline-DsLdi8sv.cjs.map +1 -0
  64. package/dist/{related-rows-B4levHcR.js → related-rows--uP2iKQC.js} +27 -17
  65. package/dist/related-rows--uP2iKQC.js.map +1 -0
  66. package/dist/{related-rows-zh5f_4QR.cjs → related-rows-MPnLCmPF.cjs} +32 -22
  67. package/dist/related-rows-MPnLCmPF.cjs.map +1 -0
  68. package/dist/{search-access-CURsWn-U.js → relation-create-C43cdPQt.js} +596 -29
  69. package/dist/relation-create-C43cdPQt.js.map +1 -0
  70. package/dist/{search-access-B7G5oR8n.cjs → relation-create-DDEYcKfw.cjs} +630 -32
  71. package/dist/relation-create-DDEYcKfw.cjs.map +1 -0
  72. package/dist/{relation-many-picker-gdAOD-qK.js → relation-many-picker-BJhaEqxz.js} +210 -107
  73. package/dist/relation-many-picker-BJhaEqxz.js.map +1 -0
  74. package/dist/{relation-many-picker-B-NCEUgX.cjs → relation-many-picker-DzuGJpEl.cjs} +218 -115
  75. package/dist/relation-many-picker-DzuGJpEl.cjs.map +1 -0
  76. package/dist/{relation-picker-BAY77ivO.js → relation-picker-D-tBBZW_.js} +40 -45
  77. package/dist/relation-picker-D-tBBZW_.js.map +1 -0
  78. package/dist/{relation-picker-Da_uhFvA.cjs → relation-picker-DaIMzf2H.cjs} +45 -50
  79. package/dist/relation-picker-DaIMzf2H.cjs.map +1 -0
  80. package/dist/{relation-picker-dialog-M0pQaWlp.cjs → relation-picker-dialog-9Vg5vvR1.cjs} +142 -40
  81. package/dist/relation-picker-dialog-9Vg5vvR1.cjs.map +1 -0
  82. package/dist/{relation-picker-dialog-hA9rYrZB.js → relation-picker-dialog-DoEY8EkH.js} +143 -41
  83. package/dist/relation-picker-dialog-DoEY8EkH.js.map +1 -0
  84. package/dist/relation-select-B8Fq4o-t.js +48 -0
  85. package/dist/relation-select-B8Fq4o-t.js.map +1 -0
  86. package/dist/relation-select-Cr3CfNLy.cjs +55 -0
  87. package/dist/relation-select-Cr3CfNLy.cjs.map +1 -0
  88. package/dist/{resource-page-router-OSQyRA5b.cjs → resource-page-router-C_kGW2o_.cjs} +552 -303
  89. package/dist/resource-page-router-C_kGW2o_.cjs.map +1 -0
  90. package/dist/{resource-page-router-T4vS5gbJ.js → resource-page-router-Diw6S6TK.js} +507 -258
  91. package/dist/resource-page-router-Diw6S6TK.js.map +1 -0
  92. package/dist/{rich-text-vTyVjDoa.cjs → rich-text-C_7JCvRo.cjs} +2 -2
  93. package/dist/{rich-text-vTyVjDoa.cjs.map → rich-text-C_7JCvRo.cjs.map} +1 -1
  94. package/dist/{rich-text-DFerywgz.js → rich-text-RcPBf6hr.js} +2 -2
  95. package/dist/{rich-text-DFerywgz.js.map → rich-text-RcPBf6hr.js.map} +1 -1
  96. package/dist/success-notices-Dl3M1fZP.cjs +46 -0
  97. package/dist/success-notices-Dl3M1fZP.cjs.map +1 -0
  98. package/dist/success-notices-LDEgEkc4.js +41 -0
  99. package/dist/success-notices-LDEgEkc4.js.map +1 -0
  100. package/dist/{toaster-C7CiYA4Z.cjs → toaster-BbByOytP.cjs} +72 -80
  101. package/dist/toaster-BbByOytP.cjs.map +1 -0
  102. package/dist/{toaster-CfCRLgR-.js → toaster-Ct_YHGs6.js} +66 -68
  103. package/dist/toaster-Ct_YHGs6.js.map +1 -0
  104. package/dist/{use-dialog-filter-state-BDcgS5Oz.js → use-dialog-filter-state-B_MqzHaT.js} +5 -4
  105. package/dist/use-dialog-filter-state-B_MqzHaT.js.map +1 -0
  106. package/dist/{use-dialog-filter-state-BBCC0OEK.cjs → use-dialog-filter-state-tNvqAAjh.cjs} +6 -5
  107. package/dist/use-dialog-filter-state-tNvqAAjh.cjs.map +1 -0
  108. package/dist/use-theme-DFlpXils.cjs.map +1 -1
  109. package/dist/use-theme-dnE3Od3I.js.map +1 -1
  110. package/package.json +11 -11
  111. package/dist/bulk-summary-CwSd6Yjb.cjs +0 -108
  112. package/dist/bulk-summary-CwSd6Yjb.cjs.map +0 -1
  113. package/dist/bulk-summary-GxMDP4bw.js +0 -108
  114. package/dist/bulk-summary-GxMDP4bw.js.map +0 -1
  115. package/dist/candidates-gate-CX2PBu6l.js +0 -63
  116. package/dist/candidates-gate-CX2PBu6l.js.map +0 -1
  117. package/dist/candidates-gate-DiFI52r7.cjs +0 -70
  118. package/dist/candidates-gate-DiFI52r7.cjs.map +0 -1
  119. package/dist/candidates-relation-picker-CBsVcnPR.js +0 -82
  120. package/dist/candidates-relation-picker-CBsVcnPR.js.map +0 -1
  121. package/dist/candidates-relation-picker-Dd_GVLlu.cjs +0 -84
  122. package/dist/candidates-relation-picker-Dd_GVLlu.cjs.map +0 -1
  123. package/dist/check-confirm-dialog-BRByTFpg.cjs.map +0 -1
  124. package/dist/check-confirm-dialog-CkaVTPT9.js.map +0 -1
  125. package/dist/dialog-B_rt9S1c.js.map +0 -1
  126. package/dist/dialog-DRNUq9EA.cjs.map +0 -1
  127. package/dist/filter-builder-B3mZFL-K.cjs.map +0 -1
  128. package/dist/filter-builder-DOnaXNfP.js.map +0 -1
  129. package/dist/flow-runtime-B6zOHYqI.cjs.map +0 -1
  130. package/dist/flow-runtime-D8h99FSg.js.map +0 -1
  131. package/dist/flow-screen-CX05b3G9.cjs.map +0 -1
  132. package/dist/flow-screen-CmbARSqg.js.map +0 -1
  133. package/dist/inline-flow-host-B7U1OJUe.cjs.map +0 -1
  134. package/dist/inline-flow-host-C3H2UpSA.js.map +0 -1
  135. package/dist/lazy-relation-picker-dialog-2oxnA6IR.js +0 -14
  136. package/dist/lazy-relation-picker-dialog-2oxnA6IR.js.map +0 -1
  137. package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs +0 -19
  138. package/dist/lazy-relation-picker-dialog-BfAGKzUG.cjs.map +0 -1
  139. package/dist/page-flow-route-CQ7HsbBm.cjs.map +0 -1
  140. package/dist/page-flow-route-CyXSvXSE.js.map +0 -1
  141. package/dist/peek-filter-CnqHwTMU.cjs +0 -51
  142. package/dist/peek-filter-CnqHwTMU.cjs.map +0 -1
  143. package/dist/peek-filter-ErLfO153.js +0 -28
  144. package/dist/peek-filter-ErLfO153.js.map +0 -1
  145. package/dist/place-node--GdsP688.js.map +0 -1
  146. package/dist/place-node-SIp4XYQH.cjs.map +0 -1
  147. package/dist/related-list-timeline-BYJKEbmA.cjs.map +0 -1
  148. package/dist/related-list-timeline-DEdfd-rr.js.map +0 -1
  149. package/dist/related-rows-B4levHcR.js.map +0 -1
  150. package/dist/related-rows-zh5f_4QR.cjs.map +0 -1
  151. package/dist/relation-create-BOaUe12G.cjs +0 -362
  152. package/dist/relation-create-BOaUe12G.cjs.map +0 -1
  153. package/dist/relation-create-Bg-eEJLs.js +0 -337
  154. package/dist/relation-create-Bg-eEJLs.js.map +0 -1
  155. package/dist/relation-many-picker-B-NCEUgX.cjs.map +0 -1
  156. package/dist/relation-many-picker-gdAOD-qK.js.map +0 -1
  157. package/dist/relation-picker-BAY77ivO.js.map +0 -1
  158. package/dist/relation-picker-Da_uhFvA.cjs.map +0 -1
  159. package/dist/relation-picker-dialog-M0pQaWlp.cjs.map +0 -1
  160. package/dist/relation-picker-dialog-hA9rYrZB.js.map +0 -1
  161. package/dist/resource-page-router-OSQyRA5b.cjs.map +0 -1
  162. package/dist/resource-page-router-T4vS5gbJ.js.map +0 -1
  163. package/dist/search-access-B7G5oR8n.cjs.map +0 -1
  164. package/dist/search-access-CURsWn-U.js.map +0 -1
  165. package/dist/shell-BPlk8hEa.js +0 -205
  166. package/dist/shell-BPlk8hEa.js.map +0 -1
  167. package/dist/shell-DxhlsPwm.cjs +0 -271
  168. package/dist/shell-DxhlsPwm.cjs.map +0 -1
  169. package/dist/toaster-C7CiYA4Z.cjs.map +0 -1
  170. package/dist/toaster-CfCRLgR-.js.map +0 -1
  171. package/dist/use-dialog-filter-state-BBCC0OEK.cjs.map +0 -1
  172. package/dist/use-dialog-filter-state-BDcgS5Oz.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"use-theme-DFlpXils.cjs","names":["twMerge","GithubSlugger","THEME_STORAGE_KEY","useState","useCallback"],"sources":["../src/lib/cn.ts","../src/markdown/anchors.ts","../src/layout/use-theme.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import GithubSlugger from 'github-slugger';\n\n/**\n * Anchor identity for documentation, in ONE place.\n *\n * Three things have to agree on what `#kak-ustroen-gtin` means: `rehype-slug`,\n * which stamps ids onto headings; the FAQ accordion, which is built from\n * headings but never goes through rehype; and whatever scrolls to a hash. When\n * they disagree the link simply does nothing — no error, no console warning,\n * the page just sits there — so the algorithm lives here and everyone imports it.\n *\n * `github-slugger` is not an extra dependency: `rehype-slug` uses it internally,\n * which is exactly why it is the right choice — matching its output by\n * reimplementing it would be a slow-motion bug in a language with case rules,\n * combining marks and a Cyrillic alphabet.\n */\n\n/** Prefix `rehype-sanitize` renames every id to. See `defaultSchema.clobberPrefix`. */\nexport const DOC_ID_PREFIX = 'user-content-';\n\n/**\n * Slug for a heading's text — the value that appears in a shareable `#hash`.\n *\n * A fresh slugger per call ON PURPOSE. `GithubSlugger` is stateful: it\n * remembers what it has emitted and disambiguates repeats as `foo-1`, `foo-2`.\n * That is right when walking one document top to bottom, and wrong here, where\n * the same heading must produce the same hash every time it is asked about.\n */\nexport function docSlug(text: string): string {\n return new GithubSlugger().slug(text);\n}\n\n/**\n * The current fragment, DECODED, without its `#`.\n *\n * `window.location.hash` hands back a percent-encoded string —\n * `#%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83…` for `#почему…` — so every\n * comparison against a slug fails, silently, for exactly the alphabet this\n * project writes its documentation in. It works perfectly in English, which is\n * the worst possible way for it to be wrong.\n */\nexport function readDocHash(): string {\n if (typeof window === 'undefined') return '';\n const raw = window.location.hash.replace(/^#/, '');\n if (raw === '') return '';\n try {\n return decodeURIComponent(raw);\n } catch {\n // A malformed escape sequence is not worth throwing over.\n return raw;\n }\n}\n\n/**\n * Find the element a hash points at.\n *\n * Tries the prefixed id first, because a heading inside sanitized Markdown is\n * the common case, then the bare one for anchors we render ourselves (the FAQ\n * accordion, which is built from React and never passes through the sanitizer).\n */\nexport function findDocAnchor(hash: string, root: Document | Element = document): Element | null {\n let id = hash.replace(/^#/, '');\n if (id === '') return null;\n try {\n id = decodeURIComponent(id);\n } catch {\n /* keep the raw form */\n }\n\n /*\n `getElementById`, NOT `querySelector('#'+id)`.\n\n A fragment is user input — it arrives from the address bar — and `#a.b:c`\n is a perfectly ordinary thing to paste and an invalid CSS selector, so\n `querySelector` throws a `SyntaxError` on it. `CSS.escape` would fix that\n where it exists, and it does not exist in jsdom, so the crash would have\n been invisible to the unit suite and live only in production. Looking an id\n up by id involves no selector parsing at all.\n */\n const doc = root.ownerDocument ?? (root as Document);\n const pick = (candidate: string): Element | null => {\n const el = doc.getElementById(candidate);\n if (!el) return null;\n // Scoped lookups must not reach outside their root — the handbook searches\n // within the open article so a stale heading elsewhere cannot win.\n return root === doc || root.contains(el) ? el : null;\n };\n return pick(DOC_ID_PREFIX + id) ?? pick(id);\n}\n\n/**\n * Bring an element to the top of ITS OWN scroll container, and nothing else.\n *\n * `scrollIntoView` cannot be used here. It scrolls EVERY scrollable ancestor,\n * the document included — and the document does scroll in this shell, because\n * `SidebarInset` is `min-h-svh` and the app's own sidebar can exceed a short\n * viewport. Clicking an anchor therefore dragged the whole page up: the app\n * header and the handbook's own search box went off the top of the screen, and\n * the layout read as broken. Caught in a screenshot, not by any assertion.\n *\n * Walking to the nearest scrollable ancestor and moving only that keeps the\n * effect where the reader expects it — inside the column they are reading.\n * `scroll-margin-top` is honoured by hand, since we are no longer going through\n * the API that would apply it.\n */\nfunction placeInScroller(target: Element): void {\n let container = target.parentElement;\n while (container) {\n const style = getComputedStyle(container);\n if (\n /(auto|scroll)/.test(style.overflowY) &&\n container.scrollHeight > container.clientHeight + 1\n ) {\n break;\n }\n container = container.parentElement;\n }\n if (!container) {\n // Nothing between here and the document scrolls; the browser's own\n // behaviour is then exactly right.\n target.scrollIntoView({ block: 'start', behavior: 'auto' });\n return;\n }\n const margin = Number.parseFloat(getComputedStyle(target).scrollMarginTop) || 0;\n const delta = target.getBoundingClientRect().top - container.getBoundingClientRect().top - margin;\n container.scrollTop += delta;\n}\n\n/**\n * Scroll to an anchor and KEEP it in view while the article settles.\n *\n * One `scrollIntoView` is not enough here. An article's height is not final\n * when its markup lands: a mermaid diagram renders asynchronously and a lazily\n * loaded image has no height until it arrives, and either one appearing ABOVE\n * the target pushes it back down — the reader watches the page scroll to the\n * right place and then drift away from it. Measured: the target ended 731px\n * down a 709px pane, i.e. just off the bottom.\n *\n * So the scroll is repeated whenever the container resizes, for a short window\n * and no longer. The window is a deliberate ceiling rather than a guess at how\n * long rendering takes: after it, the reader owns the scroll position, and\n * yanking it back because a late image loaded would be worse than being a few\n * pixels off.\n */\nexport function scrollToDocAnchor(\n hash: string,\n root: Element = document.body,\n settleMs = 1500,\n): () => void {\n let stopped = false;\n const go = () => {\n if (stopped) return;\n const target = findDocAnchor(hash, root);\n if (target) placeInScroller(target);\n };\n // Two frames: markdown → hast → DOM, then layout.\n const raf1 = requestAnimationFrame(() => requestAnimationFrame(go));\n\n const observer =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver(() => requestAnimationFrame(go));\n observer?.observe(root);\n const timer = setTimeout(() => {\n stopped = true;\n observer?.disconnect();\n }, settleMs);\n\n return () => {\n stopped = true;\n cancelAnimationFrame(raf1);\n observer?.disconnect();\n clearTimeout(timer);\n };\n}\n\n/**\n * A shareable url for an anchor on the current page — READABLE.\n *\n * `new URL(…).toString()` percent-encodes the fragment, so a Russian heading\n * becomes forty characters of `%D0%BF%D0%BE…`. That is correct and unusable:\n * the entire point of this link is being pasted into a message where a person\n * reads it, and a wall of escapes tells them nothing about where it goes.\n * Browsers accept a literal fragment and encode it themselves on navigation.\n *\n * The slug is still escaped if it contains anything that would change how the\n * url PARSES — `#`, `?`, whitespace, or a stray `%`. `github-slugger` does not\n * emit those, but this is the one place where being wrong produces a link that\n * silently points somewhere else.\n */\nexport function docShareUrl(slug: string, href: string = window.location.href): string {\n const url = new URL(href);\n const safe = /[#?%\\s]/.test(slug) ? encodeURIComponent(slug) : slug;\n return `${url.origin}${url.pathname}${url.search}#${safe}`;\n}\n","'use client';\nimport { useCallback, useEffect, useState } from 'react';\nimport { THEME_STORAGE_KEY } from './theme-storage-key.js';\n\n/**\n * Re-exported here so most consumers only need one import path. A pre-paint\n * bootstrap is the one exception — `examples/next/app/layout.tsx` imports the\n * constant from the separate, hook-free `@burojs/ui-shadcn/theme-storage-key`\n * subpath instead; see `./theme-storage-key.js` for why a Server Component\n * cannot import it from here.\n */\nexport { THEME_STORAGE_KEY };\n\n/** What the user picked. `'system'` means \"no explicit choice\" — nothing is\n * persisted for it; its absence from storage IS the `system` state. */\nexport type ThemeChoice = 'light' | 'dark' | 'system';\n\n/** Every theme choice, in the order a control offers them. */\nexport const THEME_CHOICES: readonly ThemeChoice[] = ['light', 'dark', 'system'];\n\n/** What is actually painted. Never `'system'` — every reader (Toaster,\n * CodeMirror, Mermaid) only ever needs to know light or dark. */\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface UseThemeResult {\n /** The persisted preference, including `'system'`. Only the theme\n * submenu (the one place with a three-way control) needs this. */\n choice: ThemeChoice;\n /** The resolved light/dark value, kept in sync with the `dark` class on\n * `<html>`. This is what every read-only consumer wants. */\n resolved: ResolvedTheme;\n /** Set a new choice: applies the `dark` class immediately and persists\n * (or, for `'system'`, clears) the preference. */\n setTheme: (next: ThemeChoice) => void;\n}\n\nfunction systemPrefersDark(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-color-scheme: dark)').matches\n );\n}\n\nfunction readChoice(): ThemeChoice {\n if (typeof window === 'undefined') return 'system';\n try {\n const saved = window.localStorage.getItem(THEME_STORAGE_KEY);\n if (saved === 'light' || saved === 'dark') return saved;\n } catch {\n // localStorage may be disabled — fall through to system\n }\n return 'system';\n}\n\nfunction resolveTheme(choice: ThemeChoice): ResolvedTheme {\n if (choice === 'light' || choice === 'dark') return choice;\n return systemPrefersDark() ? 'dark' : 'light';\n}\n\nfunction applyDomClass(resolved: ResolvedTheme): void {\n if (typeof document === 'undefined') return;\n document.documentElement.classList.toggle('dark', resolved === 'dark');\n}\n\nfunction persistChoice(choice: ThemeChoice): void {\n if (typeof window === 'undefined') return;\n try {\n if (choice === 'system') window.localStorage.removeItem(THEME_STORAGE_KEY);\n else window.localStorage.setItem(THEME_STORAGE_KEY, choice);\n } catch {\n // localStorage may be disabled — the class flip already applied.\n }\n}\n\ninterface ThemeState {\n choice: ThemeChoice;\n resolved: ResolvedTheme;\n}\n\n/**\n * Single source of truth for light/dark/system, replacing seven places that\n * each used to know about the theme independently (a bootstrap script, a\n * command-palette action, a submenu, an unused kit component, and three\n * MutationObserver-based readers).\n *\n * Read-only consumers (Toaster, CodeMirror, Mermaid) only need `resolved`\n * and don't call the public `setTheme` — they stay in sync via the\n * `<html class=\"dark\">` MutationObserver below, which fires regardless of\n * WHICH writer flipped it. That is deliberate: it's the same mechanism these\n * three already used individually before this hook existed, now shared\n * instead of duplicated, and it means a future writer doesn't need to know\n * about every reader. Every mounted instance — including these \"read-only\"\n * ones — still runs its OWN `onSystemChange` listener below and applies the\n * class itself on an OS change; see that listener for why re-reading storage\n * there (rather than trusting this instance's local `choice`) is required\n * for that to be safe with several instances mounted at once.\n *\n * `choice` (the three-way value, including `'system'`) is NOT synchronized\n * the same way — the `dark` class alone can't tell \"explicitly dark\" apart\n * from \"system, and the system happens to be dark\". The only consumer that\n * needs `choice` is the theme submenu, and Radix unmounts a closed dropdown's\n * content, so it re-reads storage fresh every time it opens; no live\n * cross-component sync of `choice` is needed in practice.\n *\n * NOT used by the bootstrap `<script>` in `app/layout.tsx` — see the comment\n * there for why a hook cannot do that job.\n */\nexport function useTheme(): UseThemeResult {\n // Safe default for the very first client render, corrected synchronously\n // by the effect below on mount — mirrors the pattern the three readers\n // used before migrating here (start light/false, fix it post-mount) so no\n // component newly grows a hydration-mismatch warning.\n const [state, setState] = useState<ThemeState>({ choice: 'system', resolved: 'light' });\n\n useEffect(() => {\n const choice = readChoice();\n setState({ choice, resolved: resolveTheme(choice) });\n\n const root = document.documentElement;\n const observer = new MutationObserver(() => {\n const resolved: ResolvedTheme = root.classList.contains('dark') ? 'dark' : 'light';\n setState((prev) => (prev.resolved === resolved ? prev : { ...prev, resolved }));\n });\n observer.observe(root, { attributes: true, attributeFilter: ['class'] });\n\n const mql =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)')\n : undefined;\n const onSystemChange = (event: { matches: boolean }) => {\n // Re-read the stored choice instead of trusting this instance's own\n // `prev.choice`: `choice` is captured once at mount and never\n // resynchronised, but several `useTheme()` instances mount at once\n // (Toaster, CodeMirror, Mermaid, the admin layout, the theme submenu\n // while open). If a later explicit pick only updates the instance that\n // made it, every OTHER instance still believes `system` applies and\n // would repaint the whole app back to the OS value on the next\n // preference flip — silently overriding storage, which is why this\n // must not trust local state. Storage is already the single source of\n // truth (`setTheme('system')` clears the key), so a fresh read here is\n // correct in both directions: an explicit pick is respected, and\n // `system` is honoured even once the one instance that set it has\n // unmounted. Every other live instance's listener fires on the same OS\n // change and independently reaches the same conclusion, so several\n // instances may call `applyDomClass` with the same value for one\n // event — redundant, but harmless, and not worth coordinating.\n const choice = readChoice();\n if (choice !== 'system') return;\n const resolved: ResolvedTheme = event.matches ? 'dark' : 'light';\n applyDomClass(resolved);\n setState((prev) =>\n prev.choice === choice && prev.resolved === resolved ? prev : { choice, resolved },\n );\n };\n mql?.addEventListener('change', onSystemChange);\n\n return () => {\n observer.disconnect();\n mql?.removeEventListener('change', onSystemChange);\n };\n }, []);\n\n const setTheme = useCallback((next: ThemeChoice) => {\n const resolved = resolveTheme(next);\n applyDomClass(resolved);\n persistChoice(next);\n setState({ choice: next, resolved });\n }, []);\n\n return { choice: state.choice, resolved: state.resolved, setTheme };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-theme-DFlpXils.cjs","names":["twMerge","GithubSlugger","THEME_STORAGE_KEY","useState","useCallback"],"sources":["../src/lib/cn.ts","../src/markdown/anchors.ts","../src/layout/use-theme.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import GithubSlugger from 'github-slugger';\n\n/**\n * Anchor identity for documentation, in ONE place.\n *\n * Three things have to agree on what `#kak-ustroen-gtin` means: `rehype-slug`,\n * which stamps ids onto headings; the FAQ accordion, which is built from\n * headings but never goes through rehype; and whatever scrolls to a hash. When\n * they disagree the link simply does nothing — no error, no console warning,\n * the page just sits there — so the algorithm lives here and everyone imports it.\n *\n * `github-slugger` is not an extra dependency: `rehype-slug` uses it internally,\n * which is exactly why it is the right choice — matching its output by\n * reimplementing it would be a slow-motion bug in a language with case rules,\n * combining marks and a Cyrillic alphabet.\n */\n\n/** Prefix `rehype-sanitize` renames every id to. See `defaultSchema.clobberPrefix`. */\nexport const DOC_ID_PREFIX = 'user-content-';\n\n/**\n * Slug for a heading's text — the value that appears in a shareable `#hash`.\n *\n * A fresh slugger per call ON PURPOSE. `GithubSlugger` is stateful: it\n * remembers what it has emitted and disambiguates repeats as `foo-1`, `foo-2`.\n * That is right when walking one document top to bottom, and wrong here, where\n * the same heading must produce the same hash every time it is asked about.\n */\nexport function docSlug(text: string): string {\n return new GithubSlugger().slug(text);\n}\n\n/**\n * The current fragment, DECODED, without its `#`.\n *\n * `window.location.hash` hands back a percent-encoded string —\n * `#%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83…` for a non-ASCII slug — so every\n * comparison against a slug fails, silently, for every non-ASCII alphabet. It works perfectly in ASCII-only slugs, which is\n * the worst possible way for it to be wrong.\n */\nexport function readDocHash(): string {\n if (typeof window === 'undefined') return '';\n const raw = window.location.hash.replace(/^#/, '');\n if (raw === '') return '';\n try {\n return decodeURIComponent(raw);\n } catch {\n // A malformed escape sequence is not worth throwing over.\n return raw;\n }\n}\n\n/**\n * Find the element a hash points at.\n *\n * Tries the prefixed id first, because a heading inside sanitized Markdown is\n * the common case, then the bare one for anchors we render ourselves (the FAQ\n * accordion, which is built from React and never passes through the sanitizer).\n */\nexport function findDocAnchor(hash: string, root: Document | Element = document): Element | null {\n let id = hash.replace(/^#/, '');\n if (id === '') return null;\n try {\n id = decodeURIComponent(id);\n } catch {\n /* keep the raw form */\n }\n\n /*\n `getElementById`, NOT `querySelector('#'+id)`.\n\n A fragment is user input — it arrives from the address bar — and `#a.b:c`\n is a perfectly ordinary thing to paste and an invalid CSS selector, so\n `querySelector` throws a `SyntaxError` on it. `CSS.escape` would fix that\n where it exists, and it does not exist in jsdom, so the crash would have\n been invisible to the unit suite and live only in production. Looking an id\n up by id involves no selector parsing at all.\n */\n const doc = root.ownerDocument ?? (root as Document);\n const pick = (candidate: string): Element | null => {\n const el = doc.getElementById(candidate);\n if (!el) return null;\n // Scoped lookups must not reach outside their root — the handbook searches\n // within the open article so a stale heading elsewhere cannot win.\n return root === doc || root.contains(el) ? el : null;\n };\n return pick(DOC_ID_PREFIX + id) ?? pick(id);\n}\n\n/**\n * Bring an element to the top of ITS OWN scroll container, and nothing else.\n *\n * `scrollIntoView` cannot be used here. It scrolls EVERY scrollable ancestor,\n * the document included — and the document does scroll in this shell, because\n * `SidebarInset` is `min-h-svh` and the app's own sidebar can exceed a short\n * viewport. Clicking an anchor therefore dragged the whole page up: the app\n * header and the handbook's own search box went off the top of the screen, and\n * the layout read as broken. Caught in a screenshot, not by any assertion.\n *\n * Walking to the nearest scrollable ancestor and moving only that keeps the\n * effect where the reader expects it — inside the column they are reading.\n * `scroll-margin-top` is honoured by hand, since we are no longer going through\n * the API that would apply it.\n */\nfunction placeInScroller(target: Element): void {\n let container = target.parentElement;\n while (container) {\n const style = getComputedStyle(container);\n if (\n /(auto|scroll)/.test(style.overflowY) &&\n container.scrollHeight > container.clientHeight + 1\n ) {\n break;\n }\n container = container.parentElement;\n }\n if (!container) {\n // Nothing between here and the document scrolls; the browser's own\n // behaviour is then exactly right.\n target.scrollIntoView({ block: 'start', behavior: 'auto' });\n return;\n }\n const margin = Number.parseFloat(getComputedStyle(target).scrollMarginTop) || 0;\n const delta = target.getBoundingClientRect().top - container.getBoundingClientRect().top - margin;\n container.scrollTop += delta;\n}\n\n/**\n * Scroll to an anchor and KEEP it in view while the article settles.\n *\n * One `scrollIntoView` is not enough here. An article's height is not final\n * when its markup lands: a mermaid diagram renders asynchronously and a lazily\n * loaded image has no height until it arrives, and either one appearing ABOVE\n * the target pushes it back down — the reader watches the page scroll to the\n * right place and then drift away from it. Measured: the target ended 731px\n * down a 709px pane, i.e. just off the bottom.\n *\n * So the scroll is repeated whenever the container resizes, for a short window\n * and no longer. The window is a deliberate ceiling rather than a guess at how\n * long rendering takes: after it, the reader owns the scroll position, and\n * yanking it back because a late image loaded would be worse than being a few\n * pixels off.\n */\nexport function scrollToDocAnchor(\n hash: string,\n root: Element = document.body,\n settleMs = 1500,\n): () => void {\n let stopped = false;\n const go = () => {\n if (stopped) return;\n const target = findDocAnchor(hash, root);\n if (target) placeInScroller(target);\n };\n // Two frames: markdown → hast → DOM, then layout.\n const raf1 = requestAnimationFrame(() => requestAnimationFrame(go));\n\n const observer =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver(() => requestAnimationFrame(go));\n observer?.observe(root);\n const timer = setTimeout(() => {\n stopped = true;\n observer?.disconnect();\n }, settleMs);\n\n return () => {\n stopped = true;\n cancelAnimationFrame(raf1);\n observer?.disconnect();\n clearTimeout(timer);\n };\n}\n\n/**\n * A shareable url for an anchor on the current page — READABLE.\n *\n * `new URL(…).toString()` percent-encodes the fragment, so a Russian heading\n * becomes forty characters of `%D0%BF%D0%BE…`. That is correct and unusable:\n * the entire point of this link is being pasted into a message where a person\n * reads it, and a wall of escapes tells them nothing about where it goes.\n * Browsers accept a literal fragment and encode it themselves on navigation.\n *\n * The slug is still escaped if it contains anything that would change how the\n * url PARSES — `#`, `?`, whitespace, or a stray `%`. `github-slugger` does not\n * emit those, but this is the one place where being wrong produces a link that\n * silently points somewhere else.\n */\nexport function docShareUrl(slug: string, href: string = window.location.href): string {\n const url = new URL(href);\n const safe = /[#?%\\s]/.test(slug) ? encodeURIComponent(slug) : slug;\n return `${url.origin}${url.pathname}${url.search}#${safe}`;\n}\n","'use client';\nimport { useCallback, useEffect, useState } from 'react';\nimport { THEME_STORAGE_KEY } from './theme-storage-key.js';\n\n/**\n * Re-exported here so most consumers only need one import path. A pre-paint\n * bootstrap is the one exception — `examples/next/app/layout.tsx` imports the\n * constant from the separate, hook-free `@burojs/ui-shadcn/theme-storage-key`\n * subpath instead; see `./theme-storage-key.js` for why a Server Component\n * cannot import it from here.\n */\nexport { THEME_STORAGE_KEY };\n\n/** What the user picked. `'system'` means \"no explicit choice\" — nothing is\n * persisted for it; its absence from storage IS the `system` state. */\nexport type ThemeChoice = 'light' | 'dark' | 'system';\n\n/** Every theme choice, in the order a control offers them. */\nexport const THEME_CHOICES: readonly ThemeChoice[] = ['light', 'dark', 'system'];\n\n/** What is actually painted. Never `'system'` — every reader (Toaster,\n * CodeMirror, Mermaid) only ever needs to know light or dark. */\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface UseThemeResult {\n /** The persisted preference, including `'system'`. Only the theme\n * submenu (the one place with a three-way control) needs this. */\n choice: ThemeChoice;\n /** The resolved light/dark value, kept in sync with the `dark` class on\n * `<html>`. This is what every read-only consumer wants. */\n resolved: ResolvedTheme;\n /** Set a new choice: applies the `dark` class immediately and persists\n * (or, for `'system'`, clears) the preference. */\n setTheme: (next: ThemeChoice) => void;\n}\n\nfunction systemPrefersDark(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-color-scheme: dark)').matches\n );\n}\n\nfunction readChoice(): ThemeChoice {\n if (typeof window === 'undefined') return 'system';\n try {\n const saved = window.localStorage.getItem(THEME_STORAGE_KEY);\n if (saved === 'light' || saved === 'dark') return saved;\n } catch {\n // localStorage may be disabled — fall through to system\n }\n return 'system';\n}\n\nfunction resolveTheme(choice: ThemeChoice): ResolvedTheme {\n if (choice === 'light' || choice === 'dark') return choice;\n return systemPrefersDark() ? 'dark' : 'light';\n}\n\nfunction applyDomClass(resolved: ResolvedTheme): void {\n if (typeof document === 'undefined') return;\n document.documentElement.classList.toggle('dark', resolved === 'dark');\n}\n\nfunction persistChoice(choice: ThemeChoice): void {\n if (typeof window === 'undefined') return;\n try {\n if (choice === 'system') window.localStorage.removeItem(THEME_STORAGE_KEY);\n else window.localStorage.setItem(THEME_STORAGE_KEY, choice);\n } catch {\n // localStorage may be disabled — the class flip already applied.\n }\n}\n\ninterface ThemeState {\n choice: ThemeChoice;\n resolved: ResolvedTheme;\n}\n\n/**\n * Single source of truth for light/dark/system, replacing seven places that\n * each used to know about the theme independently (a bootstrap script, a\n * command-palette action, a submenu, an unused kit component, and three\n * MutationObserver-based readers).\n *\n * Read-only consumers (Toaster, CodeMirror, Mermaid) only need `resolved`\n * and don't call the public `setTheme` — they stay in sync via the\n * `<html class=\"dark\">` MutationObserver below, which fires regardless of\n * WHICH writer flipped it. That is deliberate: it's the same mechanism these\n * three already used individually before this hook existed, now shared\n * instead of duplicated, and it means a future writer doesn't need to know\n * about every reader. Every mounted instance — including these \"read-only\"\n * ones — still runs its OWN `onSystemChange` listener below and applies the\n * class itself on an OS change; see that listener for why re-reading storage\n * there (rather than trusting this instance's local `choice`) is required\n * for that to be safe with several instances mounted at once.\n *\n * `choice` (the three-way value, including `'system'`) is NOT synchronized\n * the same way — the `dark` class alone can't tell \"explicitly dark\" apart\n * from \"system, and the system happens to be dark\". The only consumer that\n * needs `choice` is the theme submenu, and Radix unmounts a closed dropdown's\n * content, so it re-reads storage fresh every time it opens; no live\n * cross-component sync of `choice` is needed in practice.\n *\n * NOT used by the bootstrap `<script>` in `app/layout.tsx` — see the comment\n * there for why a hook cannot do that job.\n */\nexport function useTheme(): UseThemeResult {\n // Safe default for the very first client render, corrected synchronously\n // by the effect below on mount — mirrors the pattern the three readers\n // used before migrating here (start light/false, fix it post-mount) so no\n // component newly grows a hydration-mismatch warning.\n const [state, setState] = useState<ThemeState>({ choice: 'system', resolved: 'light' });\n\n useEffect(() => {\n const choice = readChoice();\n setState({ choice, resolved: resolveTheme(choice) });\n\n const root = document.documentElement;\n const observer = new MutationObserver(() => {\n const resolved: ResolvedTheme = root.classList.contains('dark') ? 'dark' : 'light';\n setState((prev) => (prev.resolved === resolved ? prev : { ...prev, resolved }));\n });\n observer.observe(root, { attributes: true, attributeFilter: ['class'] });\n\n const mql =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)')\n : undefined;\n const onSystemChange = (event: { matches: boolean }) => {\n // Re-read the stored choice instead of trusting this instance's own\n // `prev.choice`: `choice` is captured once at mount and never\n // resynchronised, but several `useTheme()` instances mount at once\n // (Toaster, CodeMirror, Mermaid, the admin layout, the theme submenu\n // while open). If a later explicit pick only updates the instance that\n // made it, every OTHER instance still believes `system` applies and\n // would repaint the whole app back to the OS value on the next\n // preference flip — silently overriding storage, which is why this\n // must not trust local state. Storage is already the single source of\n // truth (`setTheme('system')` clears the key), so a fresh read here is\n // correct in both directions: an explicit pick is respected, and\n // `system` is honoured even once the one instance that set it has\n // unmounted. Every other live instance's listener fires on the same OS\n // change and independently reaches the same conclusion, so several\n // instances may call `applyDomClass` with the same value for one\n // event — redundant, but harmless, and not worth coordinating.\n const choice = readChoice();\n if (choice !== 'system') return;\n const resolved: ResolvedTheme = event.matches ? 'dark' : 'light';\n applyDomClass(resolved);\n setState((prev) =>\n prev.choice === choice && prev.resolved === resolved ? prev : { choice, resolved },\n );\n };\n mql?.addEventListener('change', onSystemChange);\n\n return () => {\n observer.disconnect();\n mql?.removeEventListener('change', onSystemChange);\n };\n }, []);\n\n const setTheme = useCallback((next: ThemeChoice) => {\n const resolved = resolveTheme(next);\n applyDomClass(resolved);\n persistChoice(next);\n setState({ choice: next, resolved });\n }, []);\n\n return { choice: state.choice, resolved: state.resolved, setTheme };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-theme-dnE3Od3I.js","names":[],"sources":["../src/lib/cn.ts","../src/markdown/anchors.ts","../src/layout/use-theme.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import GithubSlugger from 'github-slugger';\n\n/**\n * Anchor identity for documentation, in ONE place.\n *\n * Three things have to agree on what `#kak-ustroen-gtin` means: `rehype-slug`,\n * which stamps ids onto headings; the FAQ accordion, which is built from\n * headings but never goes through rehype; and whatever scrolls to a hash. When\n * they disagree the link simply does nothing — no error, no console warning,\n * the page just sits there — so the algorithm lives here and everyone imports it.\n *\n * `github-slugger` is not an extra dependency: `rehype-slug` uses it internally,\n * which is exactly why it is the right choice — matching its output by\n * reimplementing it would be a slow-motion bug in a language with case rules,\n * combining marks and a Cyrillic alphabet.\n */\n\n/** Prefix `rehype-sanitize` renames every id to. See `defaultSchema.clobberPrefix`. */\nexport const DOC_ID_PREFIX = 'user-content-';\n\n/**\n * Slug for a heading's text — the value that appears in a shareable `#hash`.\n *\n * A fresh slugger per call ON PURPOSE. `GithubSlugger` is stateful: it\n * remembers what it has emitted and disambiguates repeats as `foo-1`, `foo-2`.\n * That is right when walking one document top to bottom, and wrong here, where\n * the same heading must produce the same hash every time it is asked about.\n */\nexport function docSlug(text: string): string {\n return new GithubSlugger().slug(text);\n}\n\n/**\n * The current fragment, DECODED, without its `#`.\n *\n * `window.location.hash` hands back a percent-encoded string —\n * `#%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83…` for `#почему…` — so every\n * comparison against a slug fails, silently, for exactly the alphabet this\n * project writes its documentation in. It works perfectly in English, which is\n * the worst possible way for it to be wrong.\n */\nexport function readDocHash(): string {\n if (typeof window === 'undefined') return '';\n const raw = window.location.hash.replace(/^#/, '');\n if (raw === '') return '';\n try {\n return decodeURIComponent(raw);\n } catch {\n // A malformed escape sequence is not worth throwing over.\n return raw;\n }\n}\n\n/**\n * Find the element a hash points at.\n *\n * Tries the prefixed id first, because a heading inside sanitized Markdown is\n * the common case, then the bare one for anchors we render ourselves (the FAQ\n * accordion, which is built from React and never passes through the sanitizer).\n */\nexport function findDocAnchor(hash: string, root: Document | Element = document): Element | null {\n let id = hash.replace(/^#/, '');\n if (id === '') return null;\n try {\n id = decodeURIComponent(id);\n } catch {\n /* keep the raw form */\n }\n\n /*\n `getElementById`, NOT `querySelector('#'+id)`.\n\n A fragment is user input — it arrives from the address bar — and `#a.b:c`\n is a perfectly ordinary thing to paste and an invalid CSS selector, so\n `querySelector` throws a `SyntaxError` on it. `CSS.escape` would fix that\n where it exists, and it does not exist in jsdom, so the crash would have\n been invisible to the unit suite and live only in production. Looking an id\n up by id involves no selector parsing at all.\n */\n const doc = root.ownerDocument ?? (root as Document);\n const pick = (candidate: string): Element | null => {\n const el = doc.getElementById(candidate);\n if (!el) return null;\n // Scoped lookups must not reach outside their root — the handbook searches\n // within the open article so a stale heading elsewhere cannot win.\n return root === doc || root.contains(el) ? el : null;\n };\n return pick(DOC_ID_PREFIX + id) ?? pick(id);\n}\n\n/**\n * Bring an element to the top of ITS OWN scroll container, and nothing else.\n *\n * `scrollIntoView` cannot be used here. It scrolls EVERY scrollable ancestor,\n * the document included — and the document does scroll in this shell, because\n * `SidebarInset` is `min-h-svh` and the app's own sidebar can exceed a short\n * viewport. Clicking an anchor therefore dragged the whole page up: the app\n * header and the handbook's own search box went off the top of the screen, and\n * the layout read as broken. Caught in a screenshot, not by any assertion.\n *\n * Walking to the nearest scrollable ancestor and moving only that keeps the\n * effect where the reader expects it — inside the column they are reading.\n * `scroll-margin-top` is honoured by hand, since we are no longer going through\n * the API that would apply it.\n */\nfunction placeInScroller(target: Element): void {\n let container = target.parentElement;\n while (container) {\n const style = getComputedStyle(container);\n if (\n /(auto|scroll)/.test(style.overflowY) &&\n container.scrollHeight > container.clientHeight + 1\n ) {\n break;\n }\n container = container.parentElement;\n }\n if (!container) {\n // Nothing between here and the document scrolls; the browser's own\n // behaviour is then exactly right.\n target.scrollIntoView({ block: 'start', behavior: 'auto' });\n return;\n }\n const margin = Number.parseFloat(getComputedStyle(target).scrollMarginTop) || 0;\n const delta = target.getBoundingClientRect().top - container.getBoundingClientRect().top - margin;\n container.scrollTop += delta;\n}\n\n/**\n * Scroll to an anchor and KEEP it in view while the article settles.\n *\n * One `scrollIntoView` is not enough here. An article's height is not final\n * when its markup lands: a mermaid diagram renders asynchronously and a lazily\n * loaded image has no height until it arrives, and either one appearing ABOVE\n * the target pushes it back down — the reader watches the page scroll to the\n * right place and then drift away from it. Measured: the target ended 731px\n * down a 709px pane, i.e. just off the bottom.\n *\n * So the scroll is repeated whenever the container resizes, for a short window\n * and no longer. The window is a deliberate ceiling rather than a guess at how\n * long rendering takes: after it, the reader owns the scroll position, and\n * yanking it back because a late image loaded would be worse than being a few\n * pixels off.\n */\nexport function scrollToDocAnchor(\n hash: string,\n root: Element = document.body,\n settleMs = 1500,\n): () => void {\n let stopped = false;\n const go = () => {\n if (stopped) return;\n const target = findDocAnchor(hash, root);\n if (target) placeInScroller(target);\n };\n // Two frames: markdown → hast → DOM, then layout.\n const raf1 = requestAnimationFrame(() => requestAnimationFrame(go));\n\n const observer =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver(() => requestAnimationFrame(go));\n observer?.observe(root);\n const timer = setTimeout(() => {\n stopped = true;\n observer?.disconnect();\n }, settleMs);\n\n return () => {\n stopped = true;\n cancelAnimationFrame(raf1);\n observer?.disconnect();\n clearTimeout(timer);\n };\n}\n\n/**\n * A shareable url for an anchor on the current page — READABLE.\n *\n * `new URL(…).toString()` percent-encodes the fragment, so a Russian heading\n * becomes forty characters of `%D0%BF%D0%BE…`. That is correct and unusable:\n * the entire point of this link is being pasted into a message where a person\n * reads it, and a wall of escapes tells them nothing about where it goes.\n * Browsers accept a literal fragment and encode it themselves on navigation.\n *\n * The slug is still escaped if it contains anything that would change how the\n * url PARSES — `#`, `?`, whitespace, or a stray `%`. `github-slugger` does not\n * emit those, but this is the one place where being wrong produces a link that\n * silently points somewhere else.\n */\nexport function docShareUrl(slug: string, href: string = window.location.href): string {\n const url = new URL(href);\n const safe = /[#?%\\s]/.test(slug) ? encodeURIComponent(slug) : slug;\n return `${url.origin}${url.pathname}${url.search}#${safe}`;\n}\n","'use client';\nimport { useCallback, useEffect, useState } from 'react';\nimport { THEME_STORAGE_KEY } from './theme-storage-key.js';\n\n/**\n * Re-exported here so most consumers only need one import path. A pre-paint\n * bootstrap is the one exception — `examples/next/app/layout.tsx` imports the\n * constant from the separate, hook-free `@burojs/ui-shadcn/theme-storage-key`\n * subpath instead; see `./theme-storage-key.js` for why a Server Component\n * cannot import it from here.\n */\nexport { THEME_STORAGE_KEY };\n\n/** What the user picked. `'system'` means \"no explicit choice\" — nothing is\n * persisted for it; its absence from storage IS the `system` state. */\nexport type ThemeChoice = 'light' | 'dark' | 'system';\n\n/** Every theme choice, in the order a control offers them. */\nexport const THEME_CHOICES: readonly ThemeChoice[] = ['light', 'dark', 'system'];\n\n/** What is actually painted. Never `'system'` — every reader (Toaster,\n * CodeMirror, Mermaid) only ever needs to know light or dark. */\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface UseThemeResult {\n /** The persisted preference, including `'system'`. Only the theme\n * submenu (the one place with a three-way control) needs this. */\n choice: ThemeChoice;\n /** The resolved light/dark value, kept in sync with the `dark` class on\n * `<html>`. This is what every read-only consumer wants. */\n resolved: ResolvedTheme;\n /** Set a new choice: applies the `dark` class immediately and persists\n * (or, for `'system'`, clears) the preference. */\n setTheme: (next: ThemeChoice) => void;\n}\n\nfunction systemPrefersDark(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-color-scheme: dark)').matches\n );\n}\n\nfunction readChoice(): ThemeChoice {\n if (typeof window === 'undefined') return 'system';\n try {\n const saved = window.localStorage.getItem(THEME_STORAGE_KEY);\n if (saved === 'light' || saved === 'dark') return saved;\n } catch {\n // localStorage may be disabled — fall through to system\n }\n return 'system';\n}\n\nfunction resolveTheme(choice: ThemeChoice): ResolvedTheme {\n if (choice === 'light' || choice === 'dark') return choice;\n return systemPrefersDark() ? 'dark' : 'light';\n}\n\nfunction applyDomClass(resolved: ResolvedTheme): void {\n if (typeof document === 'undefined') return;\n document.documentElement.classList.toggle('dark', resolved === 'dark');\n}\n\nfunction persistChoice(choice: ThemeChoice): void {\n if (typeof window === 'undefined') return;\n try {\n if (choice === 'system') window.localStorage.removeItem(THEME_STORAGE_KEY);\n else window.localStorage.setItem(THEME_STORAGE_KEY, choice);\n } catch {\n // localStorage may be disabled — the class flip already applied.\n }\n}\n\ninterface ThemeState {\n choice: ThemeChoice;\n resolved: ResolvedTheme;\n}\n\n/**\n * Single source of truth for light/dark/system, replacing seven places that\n * each used to know about the theme independently (a bootstrap script, a\n * command-palette action, a submenu, an unused kit component, and three\n * MutationObserver-based readers).\n *\n * Read-only consumers (Toaster, CodeMirror, Mermaid) only need `resolved`\n * and don't call the public `setTheme` — they stay in sync via the\n * `<html class=\"dark\">` MutationObserver below, which fires regardless of\n * WHICH writer flipped it. That is deliberate: it's the same mechanism these\n * three already used individually before this hook existed, now shared\n * instead of duplicated, and it means a future writer doesn't need to know\n * about every reader. Every mounted instance — including these \"read-only\"\n * ones — still runs its OWN `onSystemChange` listener below and applies the\n * class itself on an OS change; see that listener for why re-reading storage\n * there (rather than trusting this instance's local `choice`) is required\n * for that to be safe with several instances mounted at once.\n *\n * `choice` (the three-way value, including `'system'`) is NOT synchronized\n * the same way — the `dark` class alone can't tell \"explicitly dark\" apart\n * from \"system, and the system happens to be dark\". The only consumer that\n * needs `choice` is the theme submenu, and Radix unmounts a closed dropdown's\n * content, so it re-reads storage fresh every time it opens; no live\n * cross-component sync of `choice` is needed in practice.\n *\n * NOT used by the bootstrap `<script>` in `app/layout.tsx` — see the comment\n * there for why a hook cannot do that job.\n */\nexport function useTheme(): UseThemeResult {\n // Safe default for the very first client render, corrected synchronously\n // by the effect below on mount — mirrors the pattern the three readers\n // used before migrating here (start light/false, fix it post-mount) so no\n // component newly grows a hydration-mismatch warning.\n const [state, setState] = useState<ThemeState>({ choice: 'system', resolved: 'light' });\n\n useEffect(() => {\n const choice = readChoice();\n setState({ choice, resolved: resolveTheme(choice) });\n\n const root = document.documentElement;\n const observer = new MutationObserver(() => {\n const resolved: ResolvedTheme = root.classList.contains('dark') ? 'dark' : 'light';\n setState((prev) => (prev.resolved === resolved ? prev : { ...prev, resolved }));\n });\n observer.observe(root, { attributes: true, attributeFilter: ['class'] });\n\n const mql =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)')\n : undefined;\n const onSystemChange = (event: { matches: boolean }) => {\n // Re-read the stored choice instead of trusting this instance's own\n // `prev.choice`: `choice` is captured once at mount and never\n // resynchronised, but several `useTheme()` instances mount at once\n // (Toaster, CodeMirror, Mermaid, the admin layout, the theme submenu\n // while open). If a later explicit pick only updates the instance that\n // made it, every OTHER instance still believes `system` applies and\n // would repaint the whole app back to the OS value on the next\n // preference flip — silently overriding storage, which is why this\n // must not trust local state. Storage is already the single source of\n // truth (`setTheme('system')` clears the key), so a fresh read here is\n // correct in both directions: an explicit pick is respected, and\n // `system` is honoured even once the one instance that set it has\n // unmounted. Every other live instance's listener fires on the same OS\n // change and independently reaches the same conclusion, so several\n // instances may call `applyDomClass` with the same value for one\n // event — redundant, but harmless, and not worth coordinating.\n const choice = readChoice();\n if (choice !== 'system') return;\n const resolved: ResolvedTheme = event.matches ? 'dark' : 'light';\n applyDomClass(resolved);\n setState((prev) =>\n prev.choice === choice && prev.resolved === resolved ? prev : { choice, resolved },\n );\n };\n mql?.addEventListener('change', onSystemChange);\n\n return () => {\n observer.disconnect();\n mql?.removeEventListener('change', onSystemChange);\n };\n }, []);\n\n const setTheme = useCallback((next: ThemeChoice) => {\n const resolved = resolveTheme(next);\n applyDomClass(resolved);\n persistChoice(next);\n setState({ choice: next, resolved });\n }, []);\n\n return { choice: state.choice, resolved: state.resolved, setTheme };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-theme-dnE3Od3I.js","names":[],"sources":["../src/lib/cn.ts","../src/markdown/anchors.ts","../src/layout/use-theme.ts"],"sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import GithubSlugger from 'github-slugger';\n\n/**\n * Anchor identity for documentation, in ONE place.\n *\n * Three things have to agree on what `#kak-ustroen-gtin` means: `rehype-slug`,\n * which stamps ids onto headings; the FAQ accordion, which is built from\n * headings but never goes through rehype; and whatever scrolls to a hash. When\n * they disagree the link simply does nothing — no error, no console warning,\n * the page just sits there — so the algorithm lives here and everyone imports it.\n *\n * `github-slugger` is not an extra dependency: `rehype-slug` uses it internally,\n * which is exactly why it is the right choice — matching its output by\n * reimplementing it would be a slow-motion bug in a language with case rules,\n * combining marks and a Cyrillic alphabet.\n */\n\n/** Prefix `rehype-sanitize` renames every id to. See `defaultSchema.clobberPrefix`. */\nexport const DOC_ID_PREFIX = 'user-content-';\n\n/**\n * Slug for a heading's text — the value that appears in a shareable `#hash`.\n *\n * A fresh slugger per call ON PURPOSE. `GithubSlugger` is stateful: it\n * remembers what it has emitted and disambiguates repeats as `foo-1`, `foo-2`.\n * That is right when walking one document top to bottom, and wrong here, where\n * the same heading must produce the same hash every time it is asked about.\n */\nexport function docSlug(text: string): string {\n return new GithubSlugger().slug(text);\n}\n\n/**\n * The current fragment, DECODED, without its `#`.\n *\n * `window.location.hash` hands back a percent-encoded string —\n * `#%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83…` for a non-ASCII slug — so every\n * comparison against a slug fails, silently, for every non-ASCII alphabet. It works perfectly in ASCII-only slugs, which is\n * the worst possible way for it to be wrong.\n */\nexport function readDocHash(): string {\n if (typeof window === 'undefined') return '';\n const raw = window.location.hash.replace(/^#/, '');\n if (raw === '') return '';\n try {\n return decodeURIComponent(raw);\n } catch {\n // A malformed escape sequence is not worth throwing over.\n return raw;\n }\n}\n\n/**\n * Find the element a hash points at.\n *\n * Tries the prefixed id first, because a heading inside sanitized Markdown is\n * the common case, then the bare one for anchors we render ourselves (the FAQ\n * accordion, which is built from React and never passes through the sanitizer).\n */\nexport function findDocAnchor(hash: string, root: Document | Element = document): Element | null {\n let id = hash.replace(/^#/, '');\n if (id === '') return null;\n try {\n id = decodeURIComponent(id);\n } catch {\n /* keep the raw form */\n }\n\n /*\n `getElementById`, NOT `querySelector('#'+id)`.\n\n A fragment is user input — it arrives from the address bar — and `#a.b:c`\n is a perfectly ordinary thing to paste and an invalid CSS selector, so\n `querySelector` throws a `SyntaxError` on it. `CSS.escape` would fix that\n where it exists, and it does not exist in jsdom, so the crash would have\n been invisible to the unit suite and live only in production. Looking an id\n up by id involves no selector parsing at all.\n */\n const doc = root.ownerDocument ?? (root as Document);\n const pick = (candidate: string): Element | null => {\n const el = doc.getElementById(candidate);\n if (!el) return null;\n // Scoped lookups must not reach outside their root — the handbook searches\n // within the open article so a stale heading elsewhere cannot win.\n return root === doc || root.contains(el) ? el : null;\n };\n return pick(DOC_ID_PREFIX + id) ?? pick(id);\n}\n\n/**\n * Bring an element to the top of ITS OWN scroll container, and nothing else.\n *\n * `scrollIntoView` cannot be used here. It scrolls EVERY scrollable ancestor,\n * the document included — and the document does scroll in this shell, because\n * `SidebarInset` is `min-h-svh` and the app's own sidebar can exceed a short\n * viewport. Clicking an anchor therefore dragged the whole page up: the app\n * header and the handbook's own search box went off the top of the screen, and\n * the layout read as broken. Caught in a screenshot, not by any assertion.\n *\n * Walking to the nearest scrollable ancestor and moving only that keeps the\n * effect where the reader expects it — inside the column they are reading.\n * `scroll-margin-top` is honoured by hand, since we are no longer going through\n * the API that would apply it.\n */\nfunction placeInScroller(target: Element): void {\n let container = target.parentElement;\n while (container) {\n const style = getComputedStyle(container);\n if (\n /(auto|scroll)/.test(style.overflowY) &&\n container.scrollHeight > container.clientHeight + 1\n ) {\n break;\n }\n container = container.parentElement;\n }\n if (!container) {\n // Nothing between here and the document scrolls; the browser's own\n // behaviour is then exactly right.\n target.scrollIntoView({ block: 'start', behavior: 'auto' });\n return;\n }\n const margin = Number.parseFloat(getComputedStyle(target).scrollMarginTop) || 0;\n const delta = target.getBoundingClientRect().top - container.getBoundingClientRect().top - margin;\n container.scrollTop += delta;\n}\n\n/**\n * Scroll to an anchor and KEEP it in view while the article settles.\n *\n * One `scrollIntoView` is not enough here. An article's height is not final\n * when its markup lands: a mermaid diagram renders asynchronously and a lazily\n * loaded image has no height until it arrives, and either one appearing ABOVE\n * the target pushes it back down — the reader watches the page scroll to the\n * right place and then drift away from it. Measured: the target ended 731px\n * down a 709px pane, i.e. just off the bottom.\n *\n * So the scroll is repeated whenever the container resizes, for a short window\n * and no longer. The window is a deliberate ceiling rather than a guess at how\n * long rendering takes: after it, the reader owns the scroll position, and\n * yanking it back because a late image loaded would be worse than being a few\n * pixels off.\n */\nexport function scrollToDocAnchor(\n hash: string,\n root: Element = document.body,\n settleMs = 1500,\n): () => void {\n let stopped = false;\n const go = () => {\n if (stopped) return;\n const target = findDocAnchor(hash, root);\n if (target) placeInScroller(target);\n };\n // Two frames: markdown → hast → DOM, then layout.\n const raf1 = requestAnimationFrame(() => requestAnimationFrame(go));\n\n const observer =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver(() => requestAnimationFrame(go));\n observer?.observe(root);\n const timer = setTimeout(() => {\n stopped = true;\n observer?.disconnect();\n }, settleMs);\n\n return () => {\n stopped = true;\n cancelAnimationFrame(raf1);\n observer?.disconnect();\n clearTimeout(timer);\n };\n}\n\n/**\n * A shareable url for an anchor on the current page — READABLE.\n *\n * `new URL(…).toString()` percent-encodes the fragment, so a Russian heading\n * becomes forty characters of `%D0%BF%D0%BE…`. That is correct and unusable:\n * the entire point of this link is being pasted into a message where a person\n * reads it, and a wall of escapes tells them nothing about where it goes.\n * Browsers accept a literal fragment and encode it themselves on navigation.\n *\n * The slug is still escaped if it contains anything that would change how the\n * url PARSES — `#`, `?`, whitespace, or a stray `%`. `github-slugger` does not\n * emit those, but this is the one place where being wrong produces a link that\n * silently points somewhere else.\n */\nexport function docShareUrl(slug: string, href: string = window.location.href): string {\n const url = new URL(href);\n const safe = /[#?%\\s]/.test(slug) ? encodeURIComponent(slug) : slug;\n return `${url.origin}${url.pathname}${url.search}#${safe}`;\n}\n","'use client';\nimport { useCallback, useEffect, useState } from 'react';\nimport { THEME_STORAGE_KEY } from './theme-storage-key.js';\n\n/**\n * Re-exported here so most consumers only need one import path. A pre-paint\n * bootstrap is the one exception — `examples/next/app/layout.tsx` imports the\n * constant from the separate, hook-free `@burojs/ui-shadcn/theme-storage-key`\n * subpath instead; see `./theme-storage-key.js` for why a Server Component\n * cannot import it from here.\n */\nexport { THEME_STORAGE_KEY };\n\n/** What the user picked. `'system'` means \"no explicit choice\" — nothing is\n * persisted for it; its absence from storage IS the `system` state. */\nexport type ThemeChoice = 'light' | 'dark' | 'system';\n\n/** Every theme choice, in the order a control offers them. */\nexport const THEME_CHOICES: readonly ThemeChoice[] = ['light', 'dark', 'system'];\n\n/** What is actually painted. Never `'system'` — every reader (Toaster,\n * CodeMirror, Mermaid) only ever needs to know light or dark. */\nexport type ResolvedTheme = 'light' | 'dark';\n\nexport interface UseThemeResult {\n /** The persisted preference, including `'system'`. Only the theme\n * submenu (the one place with a three-way control) needs this. */\n choice: ThemeChoice;\n /** The resolved light/dark value, kept in sync with the `dark` class on\n * `<html>`. This is what every read-only consumer wants. */\n resolved: ResolvedTheme;\n /** Set a new choice: applies the `dark` class immediately and persists\n * (or, for `'system'`, clears) the preference. */\n setTheme: (next: ThemeChoice) => void;\n}\n\nfunction systemPrefersDark(): boolean {\n return (\n typeof window !== 'undefined' &&\n typeof window.matchMedia === 'function' &&\n window.matchMedia('(prefers-color-scheme: dark)').matches\n );\n}\n\nfunction readChoice(): ThemeChoice {\n if (typeof window === 'undefined') return 'system';\n try {\n const saved = window.localStorage.getItem(THEME_STORAGE_KEY);\n if (saved === 'light' || saved === 'dark') return saved;\n } catch {\n // localStorage may be disabled — fall through to system\n }\n return 'system';\n}\n\nfunction resolveTheme(choice: ThemeChoice): ResolvedTheme {\n if (choice === 'light' || choice === 'dark') return choice;\n return systemPrefersDark() ? 'dark' : 'light';\n}\n\nfunction applyDomClass(resolved: ResolvedTheme): void {\n if (typeof document === 'undefined') return;\n document.documentElement.classList.toggle('dark', resolved === 'dark');\n}\n\nfunction persistChoice(choice: ThemeChoice): void {\n if (typeof window === 'undefined') return;\n try {\n if (choice === 'system') window.localStorage.removeItem(THEME_STORAGE_KEY);\n else window.localStorage.setItem(THEME_STORAGE_KEY, choice);\n } catch {\n // localStorage may be disabled — the class flip already applied.\n }\n}\n\ninterface ThemeState {\n choice: ThemeChoice;\n resolved: ResolvedTheme;\n}\n\n/**\n * Single source of truth for light/dark/system, replacing seven places that\n * each used to know about the theme independently (a bootstrap script, a\n * command-palette action, a submenu, an unused kit component, and three\n * MutationObserver-based readers).\n *\n * Read-only consumers (Toaster, CodeMirror, Mermaid) only need `resolved`\n * and don't call the public `setTheme` — they stay in sync via the\n * `<html class=\"dark\">` MutationObserver below, which fires regardless of\n * WHICH writer flipped it. That is deliberate: it's the same mechanism these\n * three already used individually before this hook existed, now shared\n * instead of duplicated, and it means a future writer doesn't need to know\n * about every reader. Every mounted instance — including these \"read-only\"\n * ones — still runs its OWN `onSystemChange` listener below and applies the\n * class itself on an OS change; see that listener for why re-reading storage\n * there (rather than trusting this instance's local `choice`) is required\n * for that to be safe with several instances mounted at once.\n *\n * `choice` (the three-way value, including `'system'`) is NOT synchronized\n * the same way — the `dark` class alone can't tell \"explicitly dark\" apart\n * from \"system, and the system happens to be dark\". The only consumer that\n * needs `choice` is the theme submenu, and Radix unmounts a closed dropdown's\n * content, so it re-reads storage fresh every time it opens; no live\n * cross-component sync of `choice` is needed in practice.\n *\n * NOT used by the bootstrap `<script>` in `app/layout.tsx` — see the comment\n * there for why a hook cannot do that job.\n */\nexport function useTheme(): UseThemeResult {\n // Safe default for the very first client render, corrected synchronously\n // by the effect below on mount — mirrors the pattern the three readers\n // used before migrating here (start light/false, fix it post-mount) so no\n // component newly grows a hydration-mismatch warning.\n const [state, setState] = useState<ThemeState>({ choice: 'system', resolved: 'light' });\n\n useEffect(() => {\n const choice = readChoice();\n setState({ choice, resolved: resolveTheme(choice) });\n\n const root = document.documentElement;\n const observer = new MutationObserver(() => {\n const resolved: ResolvedTheme = root.classList.contains('dark') ? 'dark' : 'light';\n setState((prev) => (prev.resolved === resolved ? prev : { ...prev, resolved }));\n });\n observer.observe(root, { attributes: true, attributeFilter: ['class'] });\n\n const mql =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)')\n : undefined;\n const onSystemChange = (event: { matches: boolean }) => {\n // Re-read the stored choice instead of trusting this instance's own\n // `prev.choice`: `choice` is captured once at mount and never\n // resynchronised, but several `useTheme()` instances mount at once\n // (Toaster, CodeMirror, Mermaid, the admin layout, the theme submenu\n // while open). If a later explicit pick only updates the instance that\n // made it, every OTHER instance still believes `system` applies and\n // would repaint the whole app back to the OS value on the next\n // preference flip — silently overriding storage, which is why this\n // must not trust local state. Storage is already the single source of\n // truth (`setTheme('system')` clears the key), so a fresh read here is\n // correct in both directions: an explicit pick is respected, and\n // `system` is honoured even once the one instance that set it has\n // unmounted. Every other live instance's listener fires on the same OS\n // change and independently reaches the same conclusion, so several\n // instances may call `applyDomClass` with the same value for one\n // event — redundant, but harmless, and not worth coordinating.\n const choice = readChoice();\n if (choice !== 'system') return;\n const resolved: ResolvedTheme = event.matches ? 'dark' : 'light';\n applyDomClass(resolved);\n setState((prev) =>\n prev.choice === choice && prev.resolved === resolved ? prev : { choice, resolved },\n );\n };\n mql?.addEventListener('change', onSystemChange);\n\n return () => {\n observer.disconnect();\n mql?.removeEventListener('change', onSystemChange);\n };\n }, []);\n\n const setTheme = useCallback((next: ThemeChoice) => {\n const resolved = resolveTheme(next);\n applyDomClass(resolved);\n persistChoice(next);\n setState({ choice: next, resolved });\n }, []);\n\n return { choice: state.choice, resolved: state.resolved, setTheme };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burojs/ui-shadcn",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "description": "Buro: default shadcn+Tailwind UI kit with required testid attributes",
6
6
  "type": "module",
@@ -115,16 +115,16 @@
115
115
  "rehype-slug": "^6.0.0",
116
116
  "remark-directive": "^4",
117
117
  "remark-gfm": "^4",
118
- "sonner": "^1.7.0",
118
+ "sonner": "^2.0.8",
119
119
  "tailwind-merge": "^3.6.0",
120
120
  "tw-animate-css": "^1.4.0",
121
121
  "unist-util-visit": "^5",
122
122
  "zod": "^3.24.0",
123
- "@burojs/core": "0.5.0",
124
- "@burojs/field-types": "0.5.0",
125
- "@burojs/file-storage": "0.5.0",
126
- "@burojs/i18n-core": "0.5.0",
127
- "@burojs/react": "0.5.0"
123
+ "@burojs/core": "0.7.0",
124
+ "@burojs/field-types": "0.7.0",
125
+ "@burojs/file-storage": "0.7.0",
126
+ "@burojs/i18n-core": "0.7.0",
127
+ "@burojs/react": "0.7.0"
128
128
  },
129
129
  "peerDependencies": {
130
130
  "react": "^19.0.0",
@@ -147,9 +147,9 @@
147
147
  "react-dom": "^19.0.0",
148
148
  "typescript": "^5.5.0",
149
149
  "vitest": "^4.0.0",
150
- "@burojs/context": "0.5.0",
151
- "@burojs/export": "0.5.0",
152
- "@burojs/testing": "0.5.0",
150
+ "@burojs/context": "0.7.0",
151
+ "@burojs/export": "0.7.0",
152
+ "@burojs/testing": "0.7.0",
153
153
  "@burojs/tooling-eslint": "0.0.0",
154
154
  "@burojs/tooling-globals": "0.0.0",
155
155
  "@burojs/tooling-tsconfig": "0.0.0"
@@ -162,6 +162,6 @@
162
162
  "dev": "tsdown --watch",
163
163
  "test": "vitest run",
164
164
  "typecheck": "tsc --noEmit",
165
- "typecheck:tests": "tsc -p tsconfig.test.json"
165
+ "typecheck:tests": "tsc -p tsconfig.test.json && node tests/registration/run-cases.mjs"
166
166
  }
167
167
  }
@@ -1,108 +0,0 @@
1
- const require_button = require("./button-BxnnE28H.cjs");
2
- let _burojs_core = require("@burojs/core");
3
- let _burojs_react = require("@burojs/react");
4
- let react = require("react");
5
- let react_jsx_runtime = require("react/jsx-runtime");
6
- let _burojs_react_flows = require("@burojs/react/flows");
7
- //#region src/flows/bulk-summary.tsx
8
- const text = (t, key, fallback, params) => {
9
- const value = t(key, params === void 0 ? void 0 : { params });
10
- return value === key ? fallback : value;
11
- };
12
- function FlowBulkSummary({ resource, actionName, label, total, progress }) {
13
- const t = (0, _burojs_react.useOptionalT)();
14
- const caps = (0, _burojs_react.useI18nCaps)(resource);
15
- const forget = (0, _burojs_react_flows.useForgetEachAttempt)({
16
- resource,
17
- action: actionName
18
- });
19
- const [forgotten, setForgotten] = (0, react.useState)(() => new Set());
20
- const forgetText = text(t, "flow.forget", "I've checked — forget it");
21
- const testid = (0, _burojs_react.useTestIds)().bulkSummary(resource, actionName);
22
- const own = `resources.${resource}.actions.${actionName}.summary`;
23
- const params = {
24
- count: progress.success.length,
25
- total
26
- };
27
- const heading = caps.has(own) ? caps.t(own, { params }) : `${label}: ${text(t, "flow.bulkSummary", `${progress.success.length} processed`, params)} / ${total}`;
28
- const notProcessed = progress.rejected.filter((entry) => entry.reason !== "server");
29
- const refused = progress.rejected.filter((entry) => entry.reason === "server");
30
- const groups = [
31
- {
32
- key: "not-processed",
33
- title: text(t, "flow.rejected.precondition", "The record changed. Nothing was sent."),
34
- ids: notProcessed.map((entry) => entry.id)
35
- },
36
- {
37
- key: "rejected",
38
- title: text(t, "flow.rejected.validation", "The server refused the values."),
39
- ids: refused.map((entry) => entry.id)
40
- },
41
- {
42
- key: "failed",
43
- title: text(t, "flow.failed", "The request could not be sent."),
44
- ids: progress.failed
45
- },
46
- {
47
- key: "unknown",
48
- title: text(t, "flow.unknown", "It may have been applied. Nothing was resent."),
49
- ids: progress.unknown.filter((id) => !forgotten.has(id))
50
- },
51
- {
52
- key: "unresolved",
53
- title: text(t, "flow.bulkUnresolved", "Skipped: an earlier request for it may have been applied."),
54
- ids: progress.unresolved.filter((id) => !forgotten.has(id))
55
- },
56
- {
57
- key: "not-attempted",
58
- title: text(t, "flow.bulkNotAttempted", "Not attempted."),
59
- ids: progress.notAttempted
60
- }
61
- ];
62
- return (0, react_jsx_runtime.jsxs)("div", {
63
- "data-testid": testid,
64
- className: "flex flex-col gap-3 text-sm",
65
- children: [(0, react_jsx_runtime.jsx)("p", {
66
- "data-testid": `${testid}:heading`,
67
- className: "font-medium",
68
- children: heading
69
- }), groups.filter((group) => group.ids.length > 0).map((group) => (0, react_jsx_runtime.jsxs)("section", {
70
- "data-testid": `${testid}:${group.key}`,
71
- children: [(0, react_jsx_runtime.jsxs)("h3", {
72
- className: "font-medium",
73
- children: [
74
- group.title,
75
- " ",
76
- (0, react_jsx_runtime.jsxs)("span", {
77
- "data-testid": `${testid}:${group.key}:count`,
78
- className: "tabular-nums",
79
- children: [
80
- "(",
81
- group.ids.length,
82
- ")"
83
- ]
84
- })
85
- ]
86
- }), (0, react_jsx_runtime.jsxs)("ul", {
87
- className: "flex flex-wrap gap-1 text-muted-foreground",
88
- children: [group.ids.slice(0, _burojs_core.BULK_SUMMARY_IDS).map((id) => (0, react_jsx_runtime.jsxs)("li", {
89
- "data-testid": `${testid}:${group.key}:${id}`,
90
- className: "flex items-center gap-1",
91
- children: [id, group.key === "unknown" || group.key === "unresolved" ? (0, react_jsx_runtime.jsx)(require_button.Button, {
92
- type: "button",
93
- size: "sm",
94
- variant: "ghost",
95
- className: "h-6 px-1.5 text-xs",
96
- "data-testid": `${testid}:forget:${id}`,
97
- onClick: () => void forget(id).then(() => setForgotten((previous) => new Set([...previous, id]))),
98
- children: forgetText
99
- }) : null]
100
- }, id)), group.ids.length > _burojs_core.BULK_SUMMARY_IDS ? (0, react_jsx_runtime.jsxs)("li", { children: ["+", group.ids.length - _burojs_core.BULK_SUMMARY_IDS] }) : null]
101
- })]
102
- }, group.key))]
103
- });
104
- }
105
- //#endregion
106
- exports.FlowBulkSummary = FlowBulkSummary;
107
-
108
- //# sourceMappingURL=bulk-summary-CwSd6Yjb.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bulk-summary-CwSd6Yjb.cjs","names":["useOptionalT","useI18nCaps","useForgetEachAttempt","useState","useTestIds","BULK_SUMMARY_IDS","Button"],"sources":["../src/flows/bulk-summary.tsx"],"sourcesContent":["'use client';\nimport { BULK_SUMMARY_IDS, type BulkProgress } from '@burojs/core';\nimport { useI18nCaps, useOptionalT, useTestIds } from '@burojs/react';\nimport { useForgetEachAttempt } from '@burojs/react/flows';\nimport { type ReactElement, useState } from 'react';\nimport { Button } from '../primitives/button.js';\n\nexport interface FlowBulkSummaryProps {\n readonly resource: string;\n readonly actionName: string;\n /** The action's label. */\n readonly label: string;\n /** How many ids the run was given. */\n readonly total: number;\n readonly progress: BulkProgress;\n}\n\ntype Translate = ReturnType<typeof useOptionalT>;\nconst text = (t: Translate, key: string, fallback: string, params?: Record<string, unknown>) => {\n const value = t(key, params === undefined ? undefined : { params });\n return value === key ? fallback : value;\n};\n\n/**\n * A bulk flow's summary (P4 spec 4.16): the count that succeeded of the ids given (the flow's\n * own `resources.<r>.actions.<name>.summary`, `{{count}}` of `{{total}}`, else the framework's\n * count text), then one group per other outcome with its count and up to `BULK_SUMMARY_IDS` ids —\n * not processed (gone or its precondition failed), refused, failed, unknown (may have been\n * applied), unresolved from an earlier run, not attempted.\n */\nexport function FlowBulkSummary({\n resource,\n actionName,\n label,\n total,\n progress,\n}: FlowBulkSummaryProps): ReactElement {\n const t = useOptionalT();\n const caps = useI18nCaps(resource);\n // An id the run left unknown, or found unresolved, has its exit here (P4-C review I3): the user\n // checked the record, and the id's attempt is forgotten — the next run sends it again.\n const forget = useForgetEachAttempt({ resource, action: actionName });\n const [forgotten, setForgotten] = useState<ReadonlySet<string>>(() => new Set());\n const forgetText = text(t, 'flow.forget', \"I've checked — forget it\");\n const tid = useTestIds();\n const testid = tid.bulkSummary(resource, actionName);\n const own = `resources.${resource}.actions.${actionName}.summary`;\n const params = { count: progress.success.length, total };\n const heading = caps.has(own)\n ? caps.t(own, { params })\n : `${label}: ${text(t, 'flow.bulkSummary', `${progress.success.length} processed`, params)} / ${total}`;\n const notProcessed = progress.rejected.filter((entry) => entry.reason !== 'server');\n const refused = progress.rejected.filter((entry) => entry.reason === 'server');\n const groups: readonly { key: string; title: string; ids: readonly string[] }[] = [\n {\n key: 'not-processed',\n title: text(t, 'flow.rejected.precondition', 'The record changed. Nothing was sent.'),\n ids: notProcessed.map((entry) => entry.id),\n },\n {\n key: 'rejected',\n title: text(t, 'flow.rejected.validation', 'The server refused the values.'),\n ids: refused.map((entry) => entry.id),\n },\n {\n key: 'failed',\n title: text(t, 'flow.failed', 'The request could not be sent.'),\n ids: progress.failed,\n },\n {\n key: 'unknown',\n title: text(t, 'flow.unknown', 'It may have been applied. Nothing was resent.'),\n ids: progress.unknown.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'unresolved',\n title: text(\n t,\n 'flow.bulkUnresolved',\n 'Skipped: an earlier request for it may have been applied.',\n ),\n ids: progress.unresolved.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'not-attempted',\n title: text(t, 'flow.bulkNotAttempted', 'Not attempted.'),\n ids: progress.notAttempted,\n },\n ];\n return (\n <div data-testid={testid} className=\"flex flex-col gap-3 text-sm\">\n <p data-testid={`${testid}:heading`} className=\"font-medium\">\n {heading}\n </p>\n {groups\n .filter((group) => group.ids.length > 0)\n .map((group) => (\n <section key={group.key} data-testid={`${testid}:${group.key}`}>\n <h3 className=\"font-medium\">\n {group.title}{' '}\n <span data-testid={`${testid}:${group.key}:count`} className=\"tabular-nums\">\n ({group.ids.length})\n </span>\n </h3>\n <ul className=\"flex flex-wrap gap-1 text-muted-foreground\">\n {group.ids.slice(0, BULK_SUMMARY_IDS).map((id) => (\n <li\n key={id}\n data-testid={`${testid}:${group.key}:${id}`}\n className=\"flex items-center gap-1\"\n >\n {id}\n {group.key === 'unknown' || group.key === 'unresolved' ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-1.5 text-xs\"\n data-testid={`${testid}:forget:${id}`}\n onClick={() =>\n void forget(id).then(() =>\n setForgotten((previous) => new Set([...previous, id])),\n )\n }\n >\n {forgetText}\n </Button>\n ) : null}\n </li>\n ))}\n {group.ids.length > BULK_SUMMARY_IDS ? (\n <li>+{group.ids.length - BULK_SUMMARY_IDS}</li>\n ) : null}\n </ul>\n </section>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,108 +0,0 @@
1
- import { t as Button } from "./button-MWVeZaqj.js";
2
- import { BULK_SUMMARY_IDS } from "@burojs/core";
3
- import { useI18nCaps, useOptionalT, useTestIds } from "@burojs/react";
4
- import { useState } from "react";
5
- import { jsx, jsxs } from "react/jsx-runtime";
6
- import { useForgetEachAttempt } from "@burojs/react/flows";
7
- //#region src/flows/bulk-summary.tsx
8
- const text = (t, key, fallback, params) => {
9
- const value = t(key, params === void 0 ? void 0 : { params });
10
- return value === key ? fallback : value;
11
- };
12
- function FlowBulkSummary({ resource, actionName, label, total, progress }) {
13
- const t = useOptionalT();
14
- const caps = useI18nCaps(resource);
15
- const forget = useForgetEachAttempt({
16
- resource,
17
- action: actionName
18
- });
19
- const [forgotten, setForgotten] = useState(() => new Set());
20
- const forgetText = text(t, "flow.forget", "I've checked — forget it");
21
- const testid = useTestIds().bulkSummary(resource, actionName);
22
- const own = `resources.${resource}.actions.${actionName}.summary`;
23
- const params = {
24
- count: progress.success.length,
25
- total
26
- };
27
- const heading = caps.has(own) ? caps.t(own, { params }) : `${label}: ${text(t, "flow.bulkSummary", `${progress.success.length} processed`, params)} / ${total}`;
28
- const notProcessed = progress.rejected.filter((entry) => entry.reason !== "server");
29
- const refused = progress.rejected.filter((entry) => entry.reason === "server");
30
- const groups = [
31
- {
32
- key: "not-processed",
33
- title: text(t, "flow.rejected.precondition", "The record changed. Nothing was sent."),
34
- ids: notProcessed.map((entry) => entry.id)
35
- },
36
- {
37
- key: "rejected",
38
- title: text(t, "flow.rejected.validation", "The server refused the values."),
39
- ids: refused.map((entry) => entry.id)
40
- },
41
- {
42
- key: "failed",
43
- title: text(t, "flow.failed", "The request could not be sent."),
44
- ids: progress.failed
45
- },
46
- {
47
- key: "unknown",
48
- title: text(t, "flow.unknown", "It may have been applied. Nothing was resent."),
49
- ids: progress.unknown.filter((id) => !forgotten.has(id))
50
- },
51
- {
52
- key: "unresolved",
53
- title: text(t, "flow.bulkUnresolved", "Skipped: an earlier request for it may have been applied."),
54
- ids: progress.unresolved.filter((id) => !forgotten.has(id))
55
- },
56
- {
57
- key: "not-attempted",
58
- title: text(t, "flow.bulkNotAttempted", "Not attempted."),
59
- ids: progress.notAttempted
60
- }
61
- ];
62
- return jsxs("div", {
63
- "data-testid": testid,
64
- className: "flex flex-col gap-3 text-sm",
65
- children: [jsx("p", {
66
- "data-testid": `${testid}:heading`,
67
- className: "font-medium",
68
- children: heading
69
- }), groups.filter((group) => group.ids.length > 0).map((group) => jsxs("section", {
70
- "data-testid": `${testid}:${group.key}`,
71
- children: [jsxs("h3", {
72
- className: "font-medium",
73
- children: [
74
- group.title,
75
- " ",
76
- jsxs("span", {
77
- "data-testid": `${testid}:${group.key}:count`,
78
- className: "tabular-nums",
79
- children: [
80
- "(",
81
- group.ids.length,
82
- ")"
83
- ]
84
- })
85
- ]
86
- }), jsxs("ul", {
87
- className: "flex flex-wrap gap-1 text-muted-foreground",
88
- children: [group.ids.slice(0, BULK_SUMMARY_IDS).map((id) => jsxs("li", {
89
- "data-testid": `${testid}:${group.key}:${id}`,
90
- className: "flex items-center gap-1",
91
- children: [id, group.key === "unknown" || group.key === "unresolved" ? jsx(Button, {
92
- type: "button",
93
- size: "sm",
94
- variant: "ghost",
95
- className: "h-6 px-1.5 text-xs",
96
- "data-testid": `${testid}:forget:${id}`,
97
- onClick: () => void forget(id).then(() => setForgotten((previous) => new Set([...previous, id]))),
98
- children: forgetText
99
- }) : null]
100
- }, id)), group.ids.length > BULK_SUMMARY_IDS ? jsxs("li", { children: ["+", group.ids.length - BULK_SUMMARY_IDS] }) : null]
101
- })]
102
- }, group.key))]
103
- });
104
- }
105
- //#endregion
106
- export { FlowBulkSummary };
107
-
108
- //# sourceMappingURL=bulk-summary-GxMDP4bw.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bulk-summary-GxMDP4bw.js","names":[],"sources":["../src/flows/bulk-summary.tsx"],"sourcesContent":["'use client';\nimport { BULK_SUMMARY_IDS, type BulkProgress } from '@burojs/core';\nimport { useI18nCaps, useOptionalT, useTestIds } from '@burojs/react';\nimport { useForgetEachAttempt } from '@burojs/react/flows';\nimport { type ReactElement, useState } from 'react';\nimport { Button } from '../primitives/button.js';\n\nexport interface FlowBulkSummaryProps {\n readonly resource: string;\n readonly actionName: string;\n /** The action's label. */\n readonly label: string;\n /** How many ids the run was given. */\n readonly total: number;\n readonly progress: BulkProgress;\n}\n\ntype Translate = ReturnType<typeof useOptionalT>;\nconst text = (t: Translate, key: string, fallback: string, params?: Record<string, unknown>) => {\n const value = t(key, params === undefined ? undefined : { params });\n return value === key ? fallback : value;\n};\n\n/**\n * A bulk flow's summary (P4 spec 4.16): the count that succeeded of the ids given (the flow's\n * own `resources.<r>.actions.<name>.summary`, `{{count}}` of `{{total}}`, else the framework's\n * count text), then one group per other outcome with its count and up to `BULK_SUMMARY_IDS` ids —\n * not processed (gone or its precondition failed), refused, failed, unknown (may have been\n * applied), unresolved from an earlier run, not attempted.\n */\nexport function FlowBulkSummary({\n resource,\n actionName,\n label,\n total,\n progress,\n}: FlowBulkSummaryProps): ReactElement {\n const t = useOptionalT();\n const caps = useI18nCaps(resource);\n // An id the run left unknown, or found unresolved, has its exit here (P4-C review I3): the user\n // checked the record, and the id's attempt is forgotten — the next run sends it again.\n const forget = useForgetEachAttempt({ resource, action: actionName });\n const [forgotten, setForgotten] = useState<ReadonlySet<string>>(() => new Set());\n const forgetText = text(t, 'flow.forget', \"I've checked — forget it\");\n const tid = useTestIds();\n const testid = tid.bulkSummary(resource, actionName);\n const own = `resources.${resource}.actions.${actionName}.summary`;\n const params = { count: progress.success.length, total };\n const heading = caps.has(own)\n ? caps.t(own, { params })\n : `${label}: ${text(t, 'flow.bulkSummary', `${progress.success.length} processed`, params)} / ${total}`;\n const notProcessed = progress.rejected.filter((entry) => entry.reason !== 'server');\n const refused = progress.rejected.filter((entry) => entry.reason === 'server');\n const groups: readonly { key: string; title: string; ids: readonly string[] }[] = [\n {\n key: 'not-processed',\n title: text(t, 'flow.rejected.precondition', 'The record changed. Nothing was sent.'),\n ids: notProcessed.map((entry) => entry.id),\n },\n {\n key: 'rejected',\n title: text(t, 'flow.rejected.validation', 'The server refused the values.'),\n ids: refused.map((entry) => entry.id),\n },\n {\n key: 'failed',\n title: text(t, 'flow.failed', 'The request could not be sent.'),\n ids: progress.failed,\n },\n {\n key: 'unknown',\n title: text(t, 'flow.unknown', 'It may have been applied. Nothing was resent.'),\n ids: progress.unknown.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'unresolved',\n title: text(\n t,\n 'flow.bulkUnresolved',\n 'Skipped: an earlier request for it may have been applied.',\n ),\n ids: progress.unresolved.filter((id) => !forgotten.has(id)),\n },\n {\n key: 'not-attempted',\n title: text(t, 'flow.bulkNotAttempted', 'Not attempted.'),\n ids: progress.notAttempted,\n },\n ];\n return (\n <div data-testid={testid} className=\"flex flex-col gap-3 text-sm\">\n <p data-testid={`${testid}:heading`} className=\"font-medium\">\n {heading}\n </p>\n {groups\n .filter((group) => group.ids.length > 0)\n .map((group) => (\n <section key={group.key} data-testid={`${testid}:${group.key}`}>\n <h3 className=\"font-medium\">\n {group.title}{' '}\n <span data-testid={`${testid}:${group.key}:count`} className=\"tabular-nums\">\n ({group.ids.length})\n </span>\n </h3>\n <ul className=\"flex flex-wrap gap-1 text-muted-foreground\">\n {group.ids.slice(0, BULK_SUMMARY_IDS).map((id) => (\n <li\n key={id}\n data-testid={`${testid}:${group.key}:${id}`}\n className=\"flex items-center gap-1\"\n >\n {id}\n {group.key === 'unknown' || group.key === 'unresolved' ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-1.5 text-xs\"\n data-testid={`${testid}:forget:${id}`}\n onClick={() =>\n void forget(id).then(() =>\n setForgotten((previous) => new Set([...previous, id])),\n )\n }\n >\n {forgetText}\n </Button>\n ) : null}\n </li>\n ))}\n {group.ids.length > BULK_SUMMARY_IDS ? (\n <li>+{group.ids.length - BULK_SUMMARY_IDS}</li>\n ) : null}\n </ul>\n </section>\n ))}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAkBA,MAAM,QAAQ,GAAc,KAAa,UAAkB,WAAqC;CAC9F,MAAM,QAAQ,EAAE,KAAK,WAAW,KAAA,IAAY,KAAA,IAAY,EAAE,OAAO,CAAC;CAClE,OAAO,UAAU,MAAM,WAAW;AACpC;AASA,SAAgB,gBAAgB,EAC9B,UACA,YACA,OACA,OACA,YACqC;CACrC,MAAM,IAAI,aAAa;CACvB,MAAM,OAAO,YAAY,QAAQ;CAGjC,MAAM,SAAS,qBAAqB;EAAE;EAAU,QAAQ;CAAW,CAAC;CACpE,MAAM,CAAC,WAAW,gBAAgB,eAAoC,IAAI,IAAI,CAAC;CAC/E,MAAM,aAAa,KAAK,GAAG,eAAe,0BAA0B;CAEpE,MAAM,SADM,WACK,CAAC,CAAC,YAAY,UAAU,UAAU;CACnD,MAAM,MAAM,aAAa,SAAS,WAAW,WAAW;CACxD,MAAM,SAAS;EAAE,OAAO,SAAS,QAAQ;EAAQ;CAAM;CACvD,MAAM,UAAU,KAAK,IAAI,GAAG,IACxB,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,IACtB,GAAG,MAAM,IAAI,KAAK,GAAG,oBAAoB,GAAG,SAAS,QAAQ,OAAO,aAAa,MAAM,EAAE,KAAK;CAClG,MAAM,eAAe,SAAS,SAAS,QAAQ,UAAU,MAAM,WAAW,QAAQ;CAClF,MAAM,UAAU,SAAS,SAAS,QAAQ,UAAU,MAAM,WAAW,QAAQ;CAC7E,MAAM,SAA4E;EAChF;GACE,KAAK;GACL,OAAO,KAAK,GAAG,8BAA8B,uCAAuC;GACpF,KAAK,aAAa,KAAK,UAAU,MAAM,EAAE;EAC3C;EACA;GACE,KAAK;GACL,OAAO,KAAK,GAAG,4BAA4B,gCAAgC;GAC3E,KAAK,QAAQ,KAAK,UAAU,MAAM,EAAE;EACtC;EACA;GACE,KAAK;GACL,OAAO,KAAK,GAAG,eAAe,gCAAgC;GAC9D,KAAK,SAAS;EAChB;EACA;GACE,KAAK;GACL,OAAO,KAAK,GAAG,gBAAgB,+CAA+C;GAC9E,KAAK,SAAS,QAAQ,QAAQ,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;EACzD;EACA;GACE,KAAK;GACL,OAAO,KACL,GACA,uBACA,2DACF;GACA,KAAK,SAAS,WAAW,QAAQ,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC;EAC5D;EACA;GACE,KAAK;GACL,OAAO,KAAK,GAAG,yBAAyB,gBAAgB;GACxD,KAAK,SAAS;EAChB;CACF;CACA,OACE,KAAC,OAAD;EAAK,eAAa;EAAQ,WAAU;EAApC,UAAA,CACE,IAAC,KAAD;GAAG,eAAa,GAAG,OAAO;GAAW,WAAU;GAC5C,UAAA;EACA,CAAA,GACF,OACE,QAAQ,UAAU,MAAM,IAAI,SAAS,CAAC,CAAC,CACvC,KAAK,UACJ,KAAC,WAAD;GAAyB,eAAa,GAAG,OAAO,GAAG,MAAM;GAAzD,UAAA,CACE,KAAC,MAAD;IAAI,WAAU;IAAd,UAAA;KACG,MAAM;KAAO;KACd,KAAC,QAAD;MAAM,eAAa,GAAG,OAAO,GAAG,MAAM,IAAI;MAAS,WAAU;MAA7D,UAAA;OAA4E;OACxE,MAAM,IAAI;OAAO;MACf;;IACJ;GACJ,CAAA,GAAA,KAAC,MAAD;IAAI,WAAU;IAAd,UAAA,CACG,MAAM,IAAI,MAAM,GAAG,gBAAgB,CAAC,CAAC,KAAK,OACzC,KAAC,MAAD;KAEE,eAAa,GAAG,OAAO,GAAG,MAAM,IAAI,GAAG;KACvC,WAAU;KAHZ,UAAA,CAKG,IACA,MAAM,QAAQ,aAAa,MAAM,QAAQ,eACxC,IAAC,QAAD;MACE,MAAK;MACL,MAAK;MACL,SAAQ;MACR,WAAU;MACV,eAAa,GAAG,OAAO,UAAU;MACjC,eACE,KAAK,OAAO,EAAE,CAAC,CAAC,WACd,cAAc,aAAa,IAAI,IAAI,CAAC,GAAG,UAAU,EAAE,CAAC,CAAC,CACvD;MAGD,UAAA;KACK,CAAA,IACN,IACF;IArBG,GAAA,EAqBH,CACL,GACA,MAAM,IAAI,SAAS,mBAClB,KAAC,MAAD,EAAA,UAAA,CAAI,KAAE,MAAM,IAAI,SAAS,gBAAqB,EAAA,CAAA,IAC5C,IACF;GACG,CAAA,CAAA;EArCK,GAAA,MAAM,GAqCX,CACV,CACA;;AAET"}
@@ -1,63 +0,0 @@
1
- import { resolveCandidates, resolveFieldLabel } from "@burojs/core";
2
- import { useApp, useCandidatesAccess, useI18nCaps, useOptionalT, useTestIds } from "@burojs/react";
3
- import * as React from "react";
4
- //#region src/fields/candidates-gate.ts
5
- const NO_SOURCES = Object.freeze({});
6
- const NO_FILTERS = Object.freeze([]);
7
- function useCandidatesGate(input) {
8
- const { to, candidates, sources, resource, fieldName } = input;
9
- const t = useOptionalT();
10
- const app = useApp();
11
- const tid = useTestIds();
12
- const ownerCaps = useI18nCaps(resource);
13
- const targetCaps = useI18nCaps(to ?? resource);
14
- const access = useCandidatesAccess(to ?? "", candidates);
15
- const resolution = React.useMemo(() => resolveCandidates(candidates, sources ?? NO_SOURCES), [candidates, sources]);
16
- if (candidates === void 0 || candidates.length === 0) return { filters: NO_FILTERS };
17
- const testId = tid.relationCandidatesHint(resource, fieldName);
18
- if (access.status === "pending") return {
19
- filters: NO_FILTERS,
20
- gate: { kind: "pending" }
21
- };
22
- if (access.status === "forbidden") {
23
- const declared = (app.registry.get(to ?? "")?.fields)?.[access.field]?.label;
24
- const label = resolveFieldLabel(targetCaps, access.field, typeof declared === "string" ? declared : void 0);
25
- return {
26
- filters: NO_FILTERS,
27
- gate: {
28
- kind: "forbidden",
29
- testId,
30
- message: t("relationPicker.cannotSearchBy", {
31
- params: { label },
32
- defaultValue: `You cannot search by ${label}`
33
- })
34
- }
35
- };
36
- }
37
- if (resolution.status === "pending") return {
38
- filters: NO_FILTERS,
39
- gate: { kind: "pending" }
40
- };
41
- if (resolution.status === "empty") {
42
- const { source, path } = resolution.hint;
43
- const head = path[0];
44
- const declared = (source === "values" || source === "record") && head !== void 0 ? (app.registry.get(resource)?.fields)?.[head]?.label : void 0;
45
- const label = resolveFieldLabel(ownerCaps, head ?? source, typeof declared === "string" ? declared : void 0);
46
- return {
47
- filters: NO_FILTERS,
48
- gate: {
49
- kind: "empty",
50
- testId,
51
- message: t("relationPicker.chooseFirst", {
52
- params: { label },
53
- defaultValue: `Choose ${label} first`
54
- })
55
- }
56
- };
57
- }
58
- return { filters: resolution.filters };
59
- }
60
- //#endregion
61
- export { useCandidatesGate as t };
62
-
63
- //# sourceMappingURL=candidates-gate-CX2PBu6l.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"candidates-gate-CX2PBu6l.js","names":[],"sources":["../src/fields/candidates-gate.ts"],"sourcesContent":["import {\n type FixedFilterInput,\n type RelationCandidate,\n type ScreenSources,\n resolveCandidates,\n resolveFieldLabel,\n} from '@burojs/core';\nimport { useApp, useCandidatesAccess, useI18nCaps, useOptionalT, useTestIds } from '@burojs/react';\nimport * as React from 'react';\n\n/** A select that must not read its target now (P7 3.4): why, and what it tells the user. */\nexport type CandidatesGate =\n | { readonly kind: 'pending' }\n | { readonly kind: 'empty'; readonly message: string; readonly testId: string }\n | { readonly kind: 'forbidden'; readonly message: string; readonly testId: string };\n\nconst NO_SOURCES: ScreenSources = Object.freeze({});\nconst NO_FILTERS: readonly FixedFilterInput[] = Object.freeze([]);\n\n/**\n * A relation field's `candidates` as the select applies them (P7 3.4): the filters to send and,\n * when they say the target must not be read yet, the gate that tells the user why. The refs are\n * read from the owner's projected sources, so a field P5 hides reads `null` (the select asks for\n * it first, and sends nothing); a candidate on a target field the user may not view forbids the\n * read; an undecided source or permission map keeps the select disabled.\n */\nexport function useCandidatesGate(input: {\n readonly to: string | undefined;\n readonly candidates: readonly RelationCandidate[] | undefined;\n readonly sources: ScreenSources | undefined;\n readonly resource: string;\n readonly fieldName: string;\n}): { readonly filters: readonly FixedFilterInput[]; readonly gate?: CandidatesGate } {\n const { to, candidates, sources, resource, fieldName } = input;\n const t = useOptionalT();\n const app = useApp();\n const tid = useTestIds();\n const ownerCaps = useI18nCaps(resource);\n const targetCaps = useI18nCaps(to ?? resource);\n const access = useCandidatesAccess(to ?? '', candidates);\n const resolution = React.useMemo(\n () => resolveCandidates(candidates, sources ?? NO_SOURCES),\n [candidates, sources],\n );\n if (candidates === undefined || candidates.length === 0) return { filters: NO_FILTERS };\n const testId = tid.relationCandidatesHint(resource, fieldName);\n if (access.status === 'pending') return { filters: NO_FILTERS, gate: { kind: 'pending' } };\n if (access.status === 'forbidden') {\n const declared = (\n app.registry.get(to ?? '')?.fields as Record<string, { label?: unknown }> | undefined\n )?.[access.field]?.label;\n const label = resolveFieldLabel(\n targetCaps,\n access.field,\n typeof declared === 'string' ? declared : undefined,\n );\n return {\n filters: NO_FILTERS,\n gate: {\n kind: 'forbidden',\n testId,\n message: t('relationPicker.cannotSearchBy', {\n params: { label },\n defaultValue: `You cannot search by ${label}`,\n }),\n },\n };\n }\n if (resolution.status === 'pending') return { filters: NO_FILTERS, gate: { kind: 'pending' } };\n if (resolution.status === 'empty') {\n const { source, path } = resolution.hint;\n const head = path[0];\n // The field a `values` / `record` ref reads is labelled as the owner labels it; any other\n // source (`scope`, `user`) names the key it reads, humanised.\n const own = source === 'values' || source === 'record';\n const declared =\n own && head !== undefined\n ? (app.registry.get(resource)?.fields as Record<string, { label?: unknown }> | undefined)?.[\n head\n ]?.label\n : undefined;\n const label = resolveFieldLabel(\n ownerCaps,\n head ?? source,\n typeof declared === 'string' ? declared : undefined,\n );\n return {\n filters: NO_FILTERS,\n gate: {\n kind: 'empty',\n testId,\n message: t('relationPicker.chooseFirst', {\n params: { label },\n defaultValue: `Choose ${label} first`,\n }),\n },\n };\n }\n return { filters: resolution.filters };\n}\n"],"mappings":";;;;AAgBA,MAAM,aAA4B,OAAO,OAAO,CAAC,CAAC;AAClD,MAAM,aAA0C,OAAO,OAAO,CAAC,CAAC;AAShE,SAAgB,kBAAkB,OAMoD;CACpF,MAAM,EAAE,IAAI,YAAY,SAAS,UAAU,cAAc;CACzD,MAAM,IAAI,aAAa;CACvB,MAAM,MAAM,OAAO;CACnB,MAAM,MAAM,WAAW;CACvB,MAAM,YAAY,YAAY,QAAQ;CACtC,MAAM,aAAa,YAAY,MAAM,QAAQ;CAC7C,MAAM,SAAS,oBAAoB,MAAM,IAAI,UAAU;CACvD,MAAM,aAAa,MAAM,cACjB,kBAAkB,YAAY,WAAW,UAAU,GACzD,CAAC,YAAY,OAAO,CACtB;CACA,IAAI,eAAe,KAAA,KAAa,WAAW,WAAW,GAAG,OAAO,EAAE,SAAS,WAAW;CACtF,MAAM,SAAS,IAAI,uBAAuB,UAAU,SAAS;CAC7D,IAAI,OAAO,WAAW,WAAW,OAAO;EAAE,SAAS;EAAY,MAAM,EAAE,MAAM,UAAU;CAAE;CACzF,IAAI,OAAO,WAAW,aAAa;EACjC,MAAM,YACJ,IAAI,SAAS,IAAI,MAAM,EAAE,CAAC,EAAE,OAAA,GAC1B,OAAO,MAAM,EAAE;EACnB,MAAM,QAAQ,kBACZ,YACA,OAAO,OACP,OAAO,aAAa,WAAW,WAAW,KAAA,CAC5C;EACA,OAAO;GACL,SAAS;GACT,MAAM;IACJ,MAAM;IACN;IACA,SAAS,EAAE,iCAAiC;KAC1C,QAAQ,EAAE,MAAM;KAChB,cAAc,wBAAwB;IACxC,CAAC;GACH;EACF;CACF;CACA,IAAI,WAAW,WAAW,WAAW,OAAO;EAAE,SAAS;EAAY,MAAM,EAAE,MAAM,UAAU;CAAE;CAC7F,IAAI,WAAW,WAAW,SAAS;EACjC,MAAM,EAAE,QAAQ,SAAS,WAAW;EACpC,MAAM,OAAO,KAAK;EAIlB,MAAM,YADM,WAAW,YAAY,WAAW,aAErC,SAAS,KAAA,KACX,IAAI,SAAS,IAAI,QAAQ,CAAC,EAAE,OAAA,GAC3B,KACD,EAAE,QACH,KAAA;EACN,MAAM,QAAQ,kBACZ,WACA,QAAQ,QACR,OAAO,aAAa,WAAW,WAAW,KAAA,CAC5C;EACA,OAAO;GACL,SAAS;GACT,MAAM;IACJ,MAAM;IACN;IACA,SAAS,EAAE,8BAA8B;KACvC,QAAQ,EAAE,MAAM;KAChB,cAAc,UAAU,MAAM;IAChC,CAAC;GACH;EACF;CACF;CACA,OAAO,EAAE,SAAS,WAAW,QAAQ;AACvC"}