@bgub/fig-headless 0.0.0-stage → 0.1.0-alpha.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 (174) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +595 -2
  4. package/dist/accordion.d.ts +46 -0
  5. package/dist/accordion.js +2 -0
  6. package/dist/accordion.js.map +1 -0
  7. package/dist/changes--uvVfvy8.d.ts +11 -0
  8. package/dist/changes-DglRuvQa.js +2 -0
  9. package/dist/changes-DglRuvQa.js.map +1 -0
  10. package/dist/checkbox.d.ts +24 -0
  11. package/dist/checkbox.js +2 -0
  12. package/dist/checkbox.js.map +1 -0
  13. package/dist/combobox.d.ts +53 -0
  14. package/dist/combobox.js +2 -0
  15. package/dist/combobox.js.map +1 -0
  16. package/dist/command.d.ts +42 -0
  17. package/dist/command.js +2 -0
  18. package/dist/command.js.map +1 -0
  19. package/dist/composite-CG4JghHi.d.ts +4 -0
  20. package/dist/composite-CiKLX-Ti.js +2 -0
  21. package/dist/composite-CiKLX-Ti.js.map +1 -0
  22. package/dist/controllable-value-CDbRzlId.js +2 -0
  23. package/dist/controllable-value-CDbRzlId.js.map +1 -0
  24. package/dist/diagnostics-CoURcwqb.js +2 -0
  25. package/dist/diagnostics-CoURcwqb.js.map +1 -0
  26. package/dist/dialog.d.ts +44 -0
  27. package/dist/dialog.js +2 -0
  28. package/dist/dialog.js.map +1 -0
  29. package/dist/field.d.ts +35 -0
  30. package/dist/field.js +2 -0
  31. package/dist/field.js.map +1 -0
  32. package/dist/form-reset-B7G3-0OT.js +2 -0
  33. package/dist/form-reset-B7G3-0OT.js.map +1 -0
  34. package/dist/hover-HF7JXkR2.js +2 -0
  35. package/dist/hover-HF7JXkR2.js.map +1 -0
  36. package/dist/ids-Cvu36mFL.js +2 -0
  37. package/dist/ids-Cvu36mFL.js.map +1 -0
  38. package/dist/listbox-DcD6dHSg.js +2 -0
  39. package/dist/listbox-DcD6dHSg.js.map +1 -0
  40. package/dist/listbox.d.ts +32 -0
  41. package/dist/listbox.js +2 -0
  42. package/dist/listbox.js.map +1 -0
  43. package/dist/menu/context-menu.d.ts +15 -0
  44. package/dist/menu/context-menu.js +2 -0
  45. package/dist/menu/context-menu.js.map +1 -0
  46. package/dist/menu/submenu.d.ts +20 -0
  47. package/dist/menu/submenu.js +2 -0
  48. package/dist/menu/submenu.js.map +1 -0
  49. package/dist/menu-B4pKuF3z.js +2 -0
  50. package/dist/menu-B4pKuF3z.js.map +1 -0
  51. package/dist/menu.d.ts +52 -0
  52. package/dist/menu.js +1 -0
  53. package/dist/open-state-BmP__rp4.d.ts +6 -0
  54. package/dist/open-state-G79p9WiE.js +2 -0
  55. package/dist/open-state-G79p9WiE.js.map +1 -0
  56. package/dist/part-reference-B7zINPUK.js +2 -0
  57. package/dist/part-reference-B7zINPUK.js.map +1 -0
  58. package/dist/parts-I0jdefyp.js +2 -0
  59. package/dist/parts-I0jdefyp.js.map +1 -0
  60. package/dist/popover-CSPJNmHy.d.ts +37 -0
  61. package/dist/popover-UfJHN1h2.js +2 -0
  62. package/dist/popover-UfJHN1h2.js.map +1 -0
  63. package/dist/popover.d.ts +2 -0
  64. package/dist/popover.js +1 -0
  65. package/dist/popup/position.d.ts +26 -0
  66. package/dist/popup/position.js +2 -0
  67. package/dist/popup/position.js.map +1 -0
  68. package/dist/popup-state-CFSmDEwK.js +2 -0
  69. package/dist/popup-state-CFSmDEwK.js.map +1 -0
  70. package/dist/radio-group.d.ts +52 -0
  71. package/dist/radio-group.js +2 -0
  72. package/dist/radio-group.js.map +1 -0
  73. package/dist/relations-uMHtgW03.js +2 -0
  74. package/dist/relations-uMHtgW03.js.map +1 -0
  75. package/dist/select.d.ts +45 -0
  76. package/dist/select.js +2 -0
  77. package/dist/select.js.map +1 -0
  78. package/dist/slider.d.ts +35 -0
  79. package/dist/slider.js +2 -0
  80. package/dist/slider.js.map +1 -0
  81. package/dist/switch.d.ts +24 -0
  82. package/dist/switch.js +2 -0
  83. package/dist/switch.js.map +1 -0
  84. package/dist/tabs/indicator.d.ts +20 -0
  85. package/dist/tabs/indicator.js +2 -0
  86. package/dist/tabs/indicator.js.map +1 -0
  87. package/dist/tabs.d.ts +49 -0
  88. package/dist/tabs.js +2 -0
  89. package/dist/tabs.js.map +1 -0
  90. package/dist/toast.d.ts +34 -0
  91. package/dist/toast.js +2 -0
  92. package/dist/toast.js.map +1 -0
  93. package/dist/toggle-B0hOxshF.js +2 -0
  94. package/dist/toggle-B0hOxshF.js.map +1 -0
  95. package/dist/toggle-q6B-U4Nq.d.ts +29 -0
  96. package/dist/toolbar.d.ts +25 -0
  97. package/dist/toolbar.js +2 -0
  98. package/dist/toolbar.js.map +1 -0
  99. package/dist/tooltip.d.ts +31 -0
  100. package/dist/tooltip.js +2 -0
  101. package/dist/tooltip.js.map +1 -0
  102. package/dist-development/accordion.js +2 -0
  103. package/dist-development/accordion.js.map +1 -0
  104. package/dist-development/changes-DglRuvQa.js +2 -0
  105. package/dist-development/changes-DglRuvQa.js.map +1 -0
  106. package/dist-development/checkbox.js +2 -0
  107. package/dist-development/checkbox.js.map +1 -0
  108. package/dist-development/combobox.js +2 -0
  109. package/dist-development/combobox.js.map +1 -0
  110. package/dist-development/command.js +2 -0
  111. package/dist-development/command.js.map +1 -0
  112. package/dist-development/composite-DoFLgUjR.js +2 -0
  113. package/dist-development/composite-DoFLgUjR.js.map +1 -0
  114. package/dist-development/controllable-value-CDbRzlId.js +2 -0
  115. package/dist-development/controllable-value-CDbRzlId.js.map +1 -0
  116. package/dist-development/diagnostics-BkQV0MaT.js +2 -0
  117. package/dist-development/diagnostics-BkQV0MaT.js.map +1 -0
  118. package/dist-development/dialog.js +2 -0
  119. package/dist-development/dialog.js.map +1 -0
  120. package/dist-development/field.js +2 -0
  121. package/dist-development/field.js.map +1 -0
  122. package/dist-development/form-reset-B7G3-0OT.js +2 -0
  123. package/dist-development/form-reset-B7G3-0OT.js.map +1 -0
  124. package/dist-development/hover-HF7JXkR2.js +2 -0
  125. package/dist-development/hover-HF7JXkR2.js.map +1 -0
  126. package/dist-development/ids-Cvu36mFL.js +2 -0
  127. package/dist-development/ids-Cvu36mFL.js.map +1 -0
  128. package/dist-development/listbox-B-oe_0c0.js +2 -0
  129. package/dist-development/listbox-B-oe_0c0.js.map +1 -0
  130. package/dist-development/listbox.js +2 -0
  131. package/dist-development/listbox.js.map +1 -0
  132. package/dist-development/menu/context-menu.js +2 -0
  133. package/dist-development/menu/context-menu.js.map +1 -0
  134. package/dist-development/menu/submenu.js +2 -0
  135. package/dist-development/menu/submenu.js.map +1 -0
  136. package/dist-development/menu-DBxBFdYm.js +2 -0
  137. package/dist-development/menu-DBxBFdYm.js.map +1 -0
  138. package/dist-development/menu.js +1 -0
  139. package/dist-development/open-state-G79p9WiE.js +2 -0
  140. package/dist-development/open-state-G79p9WiE.js.map +1 -0
  141. package/dist-development/part-reference-B7zINPUK.js +2 -0
  142. package/dist-development/part-reference-B7zINPUK.js.map +1 -0
  143. package/dist-development/parts-I0jdefyp.js +2 -0
  144. package/dist-development/parts-I0jdefyp.js.map +1 -0
  145. package/dist-development/popover-DXxGM3tO.js +2 -0
  146. package/dist-development/popover-DXxGM3tO.js.map +1 -0
  147. package/dist-development/popover.js +1 -0
  148. package/dist-development/popup/position.js +2 -0
  149. package/dist-development/popup/position.js.map +1 -0
  150. package/dist-development/popup-state-BUYfC2nG.js +2 -0
  151. package/dist-development/popup-state-BUYfC2nG.js.map +1 -0
  152. package/dist-development/radio-group.js +2 -0
  153. package/dist-development/radio-group.js.map +1 -0
  154. package/dist-development/relations-CZpjLINW.js +2 -0
  155. package/dist-development/relations-CZpjLINW.js.map +1 -0
  156. package/dist-development/select.js +2 -0
  157. package/dist-development/select.js.map +1 -0
  158. package/dist-development/slider.js +2 -0
  159. package/dist-development/slider.js.map +1 -0
  160. package/dist-development/switch.js +2 -0
  161. package/dist-development/switch.js.map +1 -0
  162. package/dist-development/tabs/indicator.js +2 -0
  163. package/dist-development/tabs/indicator.js.map +1 -0
  164. package/dist-development/tabs.js +2 -0
  165. package/dist-development/tabs.js.map +1 -0
  166. package/dist-development/toast.js +2 -0
  167. package/dist-development/toast.js.map +1 -0
  168. package/dist-development/toggle-CIbzprg5.js +2 -0
  169. package/dist-development/toggle-CIbzprg5.js.map +1 -0
  170. package/dist-development/toolbar.js +2 -0
  171. package/dist-development/toolbar.js.map +1 -0
  172. package/dist-development/tooltip.js +2 -0
  173. package/dist-development/tooltip.js.map +1 -0
  174. package/package.json +177 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"position.js","names":[],"sources":["../../src/internal/popup-anchor.ts","../../src/popup/position.ts"],"sourcesContent":["/** Track an anchor without a permanent animation-frame loop. */\nexport function observePopupAnchor(\n anchor: HTMLElement,\n update: () => void,\n hidden: (value: boolean) => void,\n signal: AbortSignal,\n animation: boolean,\n): () => void {\n const window = anchor.ownerDocument.defaultView!;\n let rect = anchor.getBoundingClientRect();\n let movement: IntersectionObserver | undefined;\n let frame = 0;\n let partiallyClipped = false;\n\n function check(): void {\n if (signal.aborted) return;\n const next = anchor.getBoundingClientRect();\n if (\n next.x !== rect.x ||\n next.y !== rect.y ||\n next.width !== rect.width ||\n next.height !== rect.height\n ) {\n rect = next;\n update();\n watchMovement();\n }\n }\n\n function tick(): void {\n frame = 0;\n check();\n if (!signal.aborted && (animation || partiallyClipped)) {\n frame = window.requestAnimationFrame(tick);\n }\n }\n\n function watchMovement(threshold = 1): void {\n movement?.disconnect();\n if (animation || rect.width === 0 || rect.height === 0) return;\n const document = anchor.ownerDocument.documentElement;\n // Shrink the observer's root to the anchor's current bounds. Moving any\n // edge changes its intersection, even when no element changes size.\n movement = new window.IntersectionObserver(\n (entries) => {\n if (signal.aborted) return;\n const previous = rect;\n check();\n const ratio = entries.at(-1)?.intersectionRatio ?? 1;\n // Fractional root margins can round inward. Calibrate against the\n // observed ratio so a fully visible anchor doesn't become untrackable.\n if (previous === rect && threshold === 1 && ratio > 0 && ratio < 1) {\n watchMovement(ratio);\n }\n },\n {\n rootMargin: `${-rect.top}px ${rect.right - document.clientWidth}px ${rect.bottom - document.clientHeight}px ${-rect.left}px`,\n threshold: [threshold, 1],\n },\n );\n movement.observe(anchor);\n }\n\n const visibility = new window.IntersectionObserver(\n (entries) => {\n const entry = entries.at(-1);\n if (signal.aborted || entry === undefined) return;\n hidden(\n entry.intersectionRect.width <= 0 || entry.intersectionRect.height <= 0,\n );\n // A clipped anchor can move while its visible intersection stays the\n // same. Check its bounds only while clipping makes observation ambiguous.\n partiallyClipped =\n entry.intersectionRatio > 0 && entry.intersectionRatio < 1;\n check();\n if (partiallyClipped && !frame)\n frame = window.requestAnimationFrame(tick);\n },\n { threshold: [0, 1] },\n );\n visibility.observe(anchor);\n watchMovement();\n window.addEventListener(\n \"resize\",\n () => {\n check();\n watchMovement();\n },\n { signal },\n );\n if (animation) frame = window.requestAnimationFrame(tick);\n signal.addEventListener(\n \"abort\",\n () => {\n visibility.disconnect();\n movement?.disconnect();\n window.cancelAnimationFrame(frame);\n },\n { once: true },\n );\n return check;\n}\n","import {\n createMixin,\n type MixinContext,\n type MixinDescriptor,\n useBeforePaint,\n useMemo,\n} from \"@bgub/fig\";\nimport { bindPart } from \"../internal/parts.ts\";\nimport { observePopupAnchor } from \"../internal/popup-anchor.ts\";\nimport { createPartSlot } from \"../internal/registration.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\n\nexport interface PopupPositionOptions {\n /** Match the popup widget's committed open state. */\n open: boolean;\n /** Preferred side in horizontal writing modes. Defaults to bottom. */\n side?: \"top\" | \"bottom\" | \"left\" | \"right\" | \"inline-start\" | \"inline-end\";\n /** Alignment along the side. Defaults to start. */\n align?: \"start\" | \"center\" | \"end\";\n /** Distance from the anchor in CSS pixels. Defaults to 4; may be negative. */\n sideOffset?: number;\n /** Cross-axis offset in CSS pixels; end alignment reverses its sign. Defaults to 0. */\n alignOffset?: number;\n /** Space to reserve at each viewport edge, in CSS pixels. Defaults to 4. */\n collisionPadding?: number;\n /** Check anchor bounds every frame for transform animations. Defaults to false. */\n trackAnchorAnimation?: boolean;\n}\n\nexport interface PopupPositionParts {\n anchor(): MixinDescriptor;\n popup(): MixinDescriptor;\n}\n\ntype PositionRegistry = ReturnType<typeof createPositionRegistry>;\ntype Side = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nconst anchorMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, registry: PositionRegistry) => ({\n bind: bindPart(context, registry, registry.anchor.bind),\n }),\n);\nconst popupMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, registry: PositionRegistry) => ({\n bind: bindPart(context, registry, registry.bindPopup),\n }),\n);\n\n/** Optional viewport positioning for native top-layer popups. */\nexport function usePopupPosition(\n options: PopupPositionOptions,\n): PopupPositionParts {\n const changed = useRegistrationReconcile();\n const registry = useMemo(() => createPositionRegistry(changed), []);\n useBeforePaint((signal) => {\n const anchor = registry.anchor.node();\n const popup = registry.popup.node();\n const styles = registry.styles;\n if (\n !options.open ||\n anchor === null ||\n popup === null ||\n styles === undefined\n )\n return;\n const window = anchor.ownerDocument.defaultView;\n if (window === null) return;\n let frame = 0;\n let clipped = false;\n const update = () => {\n if (!signal.aborted) position(anchor, popup, styles, options, clipped);\n };\n const schedule = () => {\n if (signal.aborted || frame) return;\n frame = window.requestAnimationFrame(() => {\n frame = 0;\n checkAnchor();\n update();\n });\n };\n update();\n const observer = new window.ResizeObserver(schedule);\n observer.observe(popup);\n for (\n let node: Element | null = anchor;\n node !== null;\n node = node.parentElement\n ) {\n observer.observe(node);\n }\n const checkAnchor = observePopupAnchor(\n anchor,\n update,\n (hidden) => {\n clipped = hidden;\n update();\n },\n signal,\n options.trackAnchorAnimation ?? false,\n );\n const events = { signal, passive: true };\n anchor.ownerDocument.addEventListener(\n \"scroll\",\n (event) => {\n // Scrolling this popup changes its contents, not its placement. Nested\n // popup positioners still observe it as an ancestor scroll target.\n if (event.target !== popup) schedule();\n },\n { ...events, capture: true },\n );\n window.addEventListener(\"resize\", schedule, events);\n window.visualViewport?.addEventListener(\"resize\", schedule, events);\n window.visualViewport?.addEventListener(\"scroll\", schedule, events);\n signal.addEventListener(\n \"abort\",\n () => {\n observer.disconnect();\n window.cancelAnimationFrame(frame);\n },\n { once: true },\n );\n });\n return {\n anchor: () => anchorMixin(registry),\n popup: () => popupMixin(registry),\n };\n}\n\n/** Restore authored values before measuring; retain new inline styles from rerenders. */\nfunction ownStyles(node: HTMLElement, signal: AbortSignal) {\n const saved = new Map<\n string,\n { value: string; priority: string; written: string }\n >();\n function restore(key: string): void {\n const previous = saved.get(key);\n if (previous === undefined) return;\n if (\n node.style.getPropertyValue(key) === previous.written &&\n node.style.getPropertyPriority(key) === \"\"\n ) {\n if (previous.value)\n node.style.setProperty(key, previous.value, previous.priority);\n else node.style.removeProperty(key);\n }\n saved.delete(key);\n }\n function set(key: string, value: string): void {\n if (\n saved.get(key)?.written === value &&\n node.style.getPropertyValue(key) === value &&\n node.style.getPropertyPriority(key) === \"\"\n )\n return;\n restore(key);\n const previous = {\n value: node.style.getPropertyValue(key),\n priority: node.style.getPropertyPriority(key),\n written: value,\n };\n node.style.setProperty(key, value);\n previous.written = node.style.getPropertyValue(key);\n saved.set(key, previous);\n }\n signal.addEventListener(\n \"abort\",\n () => {\n for (const key of saved.keys()) restore(key);\n },\n { once: true },\n );\n return { set, restore };\n}\n\nfunction createPositionRegistry(changed: () => void) {\n const anchor = createPartSlot(changed);\n const popup = createPartSlot(changed);\n const registry: {\n anchor: typeof anchor;\n popup: typeof popup;\n bindPopup: typeof bindPopup;\n styles?: ReturnType<typeof ownStyles>;\n } = { anchor, popup, bindPopup };\n function bindPopup(node: HTMLElement, signal: AbortSignal): void {\n if (!popup.bind(node, signal)) return;\n const styles = ownStyles(node, signal);\n registry.styles = styles;\n const attributes = [\"data-side\", \"data-align\", \"data-anchor-hidden\"].map(\n (key) => [key, node.getAttribute(key)] as const,\n );\n signal.addEventListener(\n \"abort\",\n () => {\n for (const [key, value] of attributes) {\n if (value === null) node.removeAttribute(key);\n else node.setAttribute(key, value);\n }\n },\n { once: true },\n );\n // Prepare while closed too: showPopover() must not expose broken native\n // anchor coordinates and scroll focus there before our layout pass.\n for (const [key, value] of Object.entries({\n position: \"fixed\",\n \"position-area\": \"none\",\n \"position-try-fallbacks\": \"none\",\n top: \"4px\",\n left: \"4px\",\n right: \"auto\",\n bottom: \"auto\",\n \"margin-top\": \"0px\",\n \"margin-right\": \"0px\",\n \"margin-bottom\": \"0px\",\n \"margin-left\": \"0px\",\n \"overflow-x\": \"auto\",\n \"overflow-y\": \"auto\",\n \"box-sizing\": \"border-box\",\n }))\n styles.set(key, value);\n }\n return registry;\n}\n\nfunction position(\n anchor: HTMLElement,\n popup: HTMLElement,\n styles: ReturnType<typeof ownStyles>,\n options: PopupPositionOptions,\n clipped: boolean,\n): void {\n const window = anchor.ownerDocument.defaultView!;\n const viewport = window.visualViewport;\n const width =\n viewport?.width ?? anchor.ownerDocument.documentElement.clientWidth;\n const height =\n viewport?.height ?? anchor.ownerDocument.documentElement.clientHeight;\n const padding = Math.max(0, options.collisionPadding ?? 4);\n const px = Math.min(padding, width / 2);\n const py = Math.min(padding, height / 2);\n const left = (viewport?.offsetLeft ?? 0) + px;\n const top = (viewport?.offsetTop ?? 0) + py;\n const right = left + width - 2 * px;\n const bottom = top + height - 2 * py;\n const offset = options.sideOffset ?? 4;\n const rtl = window.getComputedStyle(anchor).direction === \"rtl\";\n const a = anchor.getBoundingClientRect();\n popup.toggleAttribute(\n \"data-anchor-hidden\",\n clipped ||\n a.width === 0 ||\n a.height === 0 ||\n a.right <= left - px ||\n a.left >= right + px ||\n a.bottom <= top - py ||\n a.top >= bottom + py,\n );\n const align = options.align ?? \"start\";\n let side = options.side ?? \"bottom\";\n if (side === \"inline-end\") side = rtl ? \"left\" : \"right\";\n if (side === \"inline-start\") side = rtl ? \"right\" : \"left\";\n const space = {\n top: clamp(a.top - top - offset, 0, bottom - top),\n bottom: clamp(bottom - a.bottom - offset, 0, bottom - top),\n left: clamp(a.left - left - offset, 0, right - left),\n right: clamp(right - a.right - offset, 0, right - left),\n };\n const opposite: Record<Side, Side> = {\n top: \"bottom\",\n bottom: \"top\",\n left: \"right\",\n right: \"left\",\n };\n styles.set(\"--anchor-width\", `${a.width}px`);\n styles.set(\"--anchor-height\", `${a.height}px`);\n styles.set(\"--available-width\", `${right - left}px`);\n styles.set(\"--available-height\", `${bottom - top}px`);\n const scrollTop = popup.scrollTop;\n const scrollLeft = popup.scrollLeft;\n styles.restore(\"max-width\");\n styles.restore(\"max-height\");\n const computed = window.getComputedStyle(popup);\n const maxWidth = computed.maxWidth;\n const maxHeight = computed.maxHeight;\n function constrain(w: number, h: number): void {\n styles.set(\"--available-width\", `${Math.max(0, w)}px`);\n styles.set(\"--available-height\", `${Math.max(0, h)}px`);\n styles.set(\n \"max-width\",\n maxWidth === \"none\" || !maxWidth\n ? `${Math.max(0, w)}px`\n : `min(${maxWidth}, ${Math.max(0, w)}px)`,\n );\n styles.set(\n \"max-height\",\n maxHeight === \"none\" || !maxHeight\n ? `${Math.max(0, h)}px`\n : `min(${maxHeight}, ${Math.max(0, h)}px)`,\n );\n }\n constrain(right - left, bottom - top);\n let box = popup.getBoundingClientRect();\n let vertical = side === \"top\" || side === \"bottom\";\n const size = vertical ? box.height : box.width;\n if (size > space[side] && space[opposite[side]] > space[side])\n side = opposite[side];\n // Keep the useful submenu fallback: stack vertically if neither horizontal\n // side has room, rather than squeeze a readable menu into a narrow column.\n if (!vertical && box.width > space[side]) {\n side =\n space.bottom >= box.height || space.bottom >= space.top\n ? \"bottom\"\n : \"top\";\n vertical = true;\n }\n constrain(\n vertical ? right - left : space[side],\n vertical ? space[side] : bottom - top,\n );\n // Metadata can affect CSS dimensions, so publish it before the final measure.\n const logical =\n options.side === \"inline-start\" || options.side === \"inline-end\";\n const renderedSide =\n logical && !vertical\n ? (side === \"left\") !== rtl\n ? \"inline-start\"\n : \"inline-end\"\n : side;\n popup.setAttribute(\"data-side\", renderedSide);\n popup.setAttribute(\"data-align\", align);\n box = popup.getBoundingClientRect();\n // Relaxing constraints for measurement can clamp scroll offsets. Restore\n // them against the final dimensions so keyboard and pointer scrolling stick.\n popup.scrollTop = scrollTop;\n popup.scrollLeft = scrollLeft;\n const start = vertical ? a.left : a.top;\n const end = vertical ? a.right : a.bottom;\n const crossSize = vertical ? box.width : box.height;\n const reversed = vertical && rtl;\n const fromEnd = (align === \"end\") !== reversed;\n let cross =\n align === \"center\"\n ? (start + end - crossSize) / 2\n : fromEnd\n ? end - crossSize\n : start;\n // Base UI offsets start toward the interior and end toward the interior's\n // opposite direction; center follows the cross axis (RTL-aware vertically).\n const alignSign = align === \"end\" ? -1 : 1;\n cross += (options.alignOffset ?? 0) * alignSign * (reversed ? -1 : 1);\n let x = vertical\n ? cross\n : side === \"left\"\n ? a.left - offset - box.width\n : a.right + offset;\n let y = vertical\n ? side === \"top\"\n ? a.top - offset - box.height\n : a.bottom + offset\n : cross;\n x = clamp(x, left, right - box.width);\n y = clamp(y, top, bottom - box.height);\n styles.set(\"left\", `${x}px`);\n styles.set(\"top\", `${y}px`);\n // Arrowless aligned popups grow from the aligned edge. Once shifted, aim\n // toward the anchor's center instead, matching Base UI's origin convention.\n const crossOrigin =\n align !== \"center\" && Math.abs((vertical ? x : y) - cross) <= 1\n ? fromEnd\n ? crossSize\n : 0\n : clamp((start + end) / 2 - (vertical ? x : y), 0, crossSize);\n const originX = vertical\n ? crossOrigin\n : (side === \"left\" ? a.left : a.right) - x;\n const originY = vertical\n ? (side === \"top\" ? a.top : a.bottom) - y\n : crossOrigin;\n styles.set(\"--transform-origin\", `${originX}px ${originY}px`);\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(value, max));\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{o as e}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{t}from"./open-state-G79p9WiE.js";import{useBeforeLayout as n,useMemo as r,useStableEvent as i}from"@bgub/fig";function a(t,n){let r=e(t),i=e(t);function a(e){let t=e===`source`?i.items():r.items().filter(t=>t.value===e);return`${n}${e}`,t.at(-1)?.node}function o(e,t){let n=a(`anchor`);n?.style.setProperty(`anchor-name`,t);let r=a(`popup`);if(r!==void 0){if(r.style.setProperty(`position-anchor`,t),typeof r.showPopover!=`function`){r.hidden=!e;return}if(e!==r.matches(`:popover-open`)){if(e){let e=a(`source`)??n;r.showPopover(e===void 0?void 0:{source:e})}else r.hidePopover()}}}return{anchor:()=>a(`anchor`),bindAnchor:(e,t)=>r.bind(e,t,`anchor`),bindPopup:(e,t)=>r.bind(e,t,`popup`),bindSource:(e,t)=>i.bind(e,t,`source`),popup:()=>a(`popup`),supported:()=>typeof a(`popup`)?.showPopover==`function`,sync:o}}function o(e){let t=e.newState;if(t===`open`)return!0;if(t===`closed`)return!1}function s(e){let a=t(e),s=r(()=>({requested:a.open,native:void 0}),[]),c=r(()=>new WeakSet,[]);n(()=>{e.open!==void 0&&(s.requested=a.open),a.open===s.requested&&(s.native=void 0)});let l=i((e,t,n)=>{let r=a.requestOpen(e,t,n);return r&&(s.requested=e),r}),u=i(e=>{a.setOpen(e)&&(s.requested=e)}),d=i(e=>{if(e.defaultPrevented)return;if(e.target!==null){if(e.type===`beforetoggle`)c.add(e.target);else if(c.has(e.target))return}let t=o(e);if(t!==void 0){if(t!==s.requested&&!l(t,e,void 0)){e.preventDefault();return}s.native=t}});return{open:a.open,requestOpen:l,setOpen:u,nativeToggle:d,getOpen:()=>s.native??a.open}}export{a as n,o as r,s as t};
2
+ //# sourceMappingURL=popup-state-CFSmDEwK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"popup-state-CFSmDEwK.js","names":[],"sources":["../src/internal/anchored-popup.ts","../src/internal/popup-state.ts"],"sourcesContent":["import { assertSinglePart } from \"./diagnostics.ts\";\nimport { createPartCollection } from \"./registration.ts\";\n\n/** Native top-layer synchronization shared by anchored popup widgets. */\nexport function createAnchoredPopup(\n registrationChanged: () => void,\n name: string,\n) {\n const parts = createPartCollection<\"anchor\" | \"popup\">(registrationChanged);\n // The same host may own both roles. Collections key by host, so source\n // registrations must not replace the CSS anchor registration.\n const sources = createPartCollection<\"source\">(registrationChanged);\n\n function part(kind: \"anchor\" | \"popup\" | \"source\"): HTMLElement | undefined {\n const matches =\n kind === \"source\"\n ? sources.items()\n : parts.items().filter((entry) => entry.value === kind);\n assertSinglePart(matches, `${name} ${kind}`);\n return matches.at(-1)?.node;\n }\n\n function sync(open: boolean, anchorName: string): void {\n const anchor = part(\"anchor\");\n anchor?.style.setProperty(\"anchor-name\", anchorName);\n const popup = part(\"popup\");\n if (popup === undefined) return;\n popup.style.setProperty(\"position-anchor\", anchorName);\n if (typeof popup.showPopover !== \"function\") {\n popup.hidden = !open;\n return;\n }\n if (open === popup.matches(\":popover-open\")) return;\n // The source establishes the native popover tree even when a child popup\n // is mounted outside its parent popup (for example through a portal).\n if (open) {\n const source = part(\"source\") ?? anchor;\n popup.showPopover(source === undefined ? undefined : { source });\n } else popup.hidePopover();\n }\n\n return {\n anchor: () => part(\"anchor\"),\n bindAnchor: (node: HTMLElement, signal: AbortSignal) =>\n parts.bind(node, signal, \"anchor\"),\n bindPopup: (node: HTMLElement, signal: AbortSignal) =>\n parts.bind(node, signal, \"popup\"),\n bindSource: (node: HTMLElement, signal: AbortSignal) =>\n sources.bind(node, signal, \"source\"),\n popup: () => part(\"popup\"),\n supported: () => typeof part(\"popup\")?.showPopover === \"function\",\n sync,\n };\n}\n\nexport function toggledOpen(event: Event): boolean | undefined {\n const state = (event as Event & { newState?: unknown }).newState;\n if (state === \"open\") return true;\n if (state === \"closed\") return false;\n return undefined;\n}\n","import { useBeforeLayout, useMemo, useStableEvent } from \"@bgub/fig\";\nimport { toggledOpen } from \"./anchored-popup.ts\";\nimport { type OpenStateOptions, useOpenState } from \"./open-state.ts\";\n\n/**\n * Native popovers can dismiss during another widget's commit. Keep that\n * observation separate from rendered visibility and imperative intent, so a\n * stale layout effect cannot reopen a dismissed peer or reveal suspended UI.\n */\nexport function usePopupState(options: OpenStateOptions) {\n const state = useOpenState(options);\n const tracker = useMemo(\n () => ({ requested: state.open, native: undefined as boolean | undefined }),\n [],\n );\n const beforeToggleSources = useMemo(() => new WeakSet<EventTarget>(), []);\n useBeforeLayout(() => {\n if (options.open !== undefined) tracker.requested = state.open;\n if (state.open === tracker.requested) tracker.native = undefined;\n });\n const requestOpen = useStableEvent(\n (next: boolean, event: Event, trigger: Element | undefined) => {\n const accepted = state.requestOpen(next, event, trigger);\n if (accepted) tracker.requested = next;\n return accepted;\n },\n );\n const setOpen = useStableEvent((next: boolean) => {\n if (state.setOpen(next)) tracker.requested = next;\n });\n const nativeToggle = useStableEvent((event: Event) => {\n if (event.defaultPrevented) return;\n // beforetoggle is synchronous; toggle is a queued notification that can\n // arrive after a newer request. Once a host reports beforetoggle, its\n // later toggle notifications must not become fresh requests.\n if (event.target !== null) {\n if (event.type === \"beforetoggle\") beforeToggleSources.add(event.target);\n else if (beforeToggleSources.has(event.target)) return;\n }\n const next = toggledOpen(event);\n if (next === undefined) return;\n // Before/after notifications describe one transition, not two requests.\n if (next !== tracker.requested && !requestOpen(next, event, undefined)) {\n event.preventDefault();\n return;\n }\n tracker.native = next;\n });\n return {\n open: state.open,\n requestOpen,\n setOpen,\n nativeToggle,\n getOpen: () => tracker.native ?? state.open,\n };\n}\n"],"mappings":"+LAIA,SAAgB,EACd,EACA,EACA,CACA,IAAM,EAAQ,EAAyC,CAAmB,EAGpE,EAAU,EAA+B,CAAmB,EAElE,SAAS,EAAK,EAA8D,CAC1E,IAAM,EACJ,IAAS,SACL,EAAQ,MAAM,EACd,EAAM,MAAM,CAAC,CAAC,OAAQ,GAAU,EAAM,QAAU,CAAI,EAE1D,MAD0B,GAAG,EAAH,EAAW,EAAX,EACnB,EAAQ,GAAG,EAAE,CAAC,EAAE,IACzB,CAEA,SAAS,EAAK,EAAe,EAA0B,CACrD,IAAM,EAAS,EAAK,QAAQ,EAC5B,GAAQ,MAAM,YAAY,cAAe,CAAU,EACnD,IAAM,EAAQ,EAAK,OAAO,EACtB,OAAU,IAAA,GAEd,IADA,EAAM,MAAM,YAAY,kBAAmB,CAAU,EACjD,OAAO,EAAM,aAAgB,WAAY,CAC3C,EAAM,OAAS,CAAC,EAChB,MACF,CACI,OAAS,EAAM,QAAQ,eAAe,EAG1C,IAAI,EAAM,CACR,IAAM,EAAS,EAAK,QAAQ,GAAK,EACjC,EAAM,YAAY,IAAW,IAAA,GAAY,IAAA,GAAY,CAAE,QAAO,CAAC,CACjE,MAAO,EAAM,YAAY,CAAA,CAPzB,CAQF,CAEA,MAAO,CACL,WAAc,EAAK,QAAQ,EAC3B,YAAa,EAAmB,IAC9B,EAAM,KAAK,EAAM,EAAQ,QAAQ,EACnC,WAAY,EAAmB,IAC7B,EAAM,KAAK,EAAM,EAAQ,OAAO,EAClC,YAAa,EAAmB,IAC9B,EAAQ,KAAK,EAAM,EAAQ,QAAQ,EACrC,UAAa,EAAK,OAAO,EACzB,cAAiB,OAAO,EAAK,OAAO,CAAC,EAAE,aAAgB,WACvD,MACF,CACF,CAEA,SAAgB,EAAY,EAAmC,CAC7D,IAAM,EAAS,EAAyC,SACxD,GAAI,IAAU,OAAQ,MAAO,GAC7B,GAAI,IAAU,SAAU,MAAO,EAEjC,CCnDA,SAAgB,EAAc,EAA2B,CACvD,IAAM,EAAQ,EAAa,CAAO,EAC5B,EAAU,OACP,CAAE,UAAW,EAAM,KAAM,OAAQ,IAAA,EAAiC,GACzE,CAAC,CACH,EACM,EAAsB,MAAc,IAAI,QAAwB,CAAC,CAAC,EACxE,MAAsB,CAChB,EAAQ,OAAS,IAAA,KAAW,EAAQ,UAAY,EAAM,MACtD,EAAM,OAAS,EAAQ,YAAW,EAAQ,OAAS,IAAA,GACzD,CAAC,EACD,IAAM,EAAc,GACjB,EAAe,EAAc,IAAiC,CAC7D,IAAM,EAAW,EAAM,YAAY,EAAM,EAAO,CAAO,EAEvD,OADI,IAAU,EAAQ,UAAY,GAC3B,CACT,CACF,EACM,EAAU,EAAgB,GAAkB,CAC5C,EAAM,QAAQ,CAAI,IAAG,EAAQ,UAAY,EAC/C,CAAC,EACK,EAAe,EAAgB,GAAiB,CACpD,GAAI,EAAM,iBAAkB,OAI5B,GAAI,EAAM,SAAW,KAAM,CACzB,GAAI,EAAM,OAAS,eAAgB,EAAoB,IAAI,EAAM,MAAM,OAClE,GAAI,EAAoB,IAAI,EAAM,MAAM,EAAG,MAClD,CACA,IAAM,EAAO,EAAY,CAAK,EAC1B,OAAS,IAAA,GAEb,IAAI,IAAS,EAAQ,WAAa,CAAC,EAAY,EAAM,EAAO,IAAA,EAAS,EAAG,CACtE,EAAM,eAAe,EACrB,MACF,CACA,EAAQ,OAAS,CADjB,CAEF,CAAC,EACD,MAAO,CACL,KAAM,EAAM,KACZ,cACA,UACA,eACA,YAAe,EAAQ,QAAU,EAAM,IACzC,CACF"}
@@ -0,0 +1,52 @@
1
+ import { t as ChangeDetails } from "./changes--uvVfvy8.js";
2
+ import { t as Orientation } from "./composite-CG4JghHi.js";
3
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
4
+ //#region src/radio-group/radio-group.d.ts
5
+ type RadioGroupOrientation = Orientation;
6
+ type RadioGroupValueChangeDetails = ChangeDetails;
7
+ type RadioGroupValueChangeHandler<Value = unknown> = (value: Value | null, details: RadioGroupValueChangeDetails, signal: AbortSignal) => void;
8
+ interface RadioOptions {
9
+ disabled?: boolean;
10
+ }
11
+ interface RadioGroupOptions<Value = unknown> {
12
+ defaultValue?: Value | null;
13
+ /** Disables every radio in the group. */
14
+ disabled?: boolean;
15
+ /** The submitted field name. Generated when absent. */
16
+ name?: string;
17
+ onValueChange?: RadioGroupValueChangeHandler<Value>;
18
+ /** Labels the group's axis. Defaults to `vertical`, the ARIA default. */
19
+ orientation?: RadioGroupOrientation;
20
+ /** Prevents user changes while keeping radios focusable and submitted. */
21
+ readOnly?: boolean;
22
+ /** Requires a choice before the form submits. */
23
+ required?: boolean;
24
+ value?: Value | null;
25
+ }
26
+ interface RadioGroupParts<Value = unknown> {
27
+ /** The field name every radio shares. */
28
+ readonly name: string;
29
+ readonly value: Value | null;
30
+ /** Applies to a native `<input type="radio">`. */
31
+ radio(value: Value, options?: RadioOptions): MixinDescriptor;
32
+ root(): MixinDescriptor;
33
+ }
34
+ interface RadioGroupProps<Value = unknown> extends RadioGroupOptions<Value> {
35
+ children: (group: RadioGroupParts<Value>) => FigNode;
36
+ }
37
+ /**
38
+ * Coordinates one radio group over native radio inputs.
39
+ *
40
+ * Radios sharing a name are already a group to the browser, which owns the
41
+ * single tab stop, arrow movement on either axis, wrapping, skipping disabled
42
+ * radios, Space, form submission, and validity. The widget owns the selected
43
+ * value and reports changes, and adds no keyboard handling of its own.
44
+ */
45
+ declare function useRadioGroup<Value = unknown>(options?: RadioGroupOptions<Value>): RadioGroupParts<Value>;
46
+ /**
47
+ * {@link useRadioGroup} as a component, for a group that is not already a
48
+ * component of its own. Selection re-renders this root rather than the caller.
49
+ */
50
+ declare function RadioGroup<Value = unknown>(props: RadioGroupProps<Value>): FigNode;
51
+ //#endregion
52
+ export { RadioGroup, RadioGroupOptions, RadioGroupOrientation, RadioGroupParts, RadioGroupProps, RadioGroupValueChangeDetails, RadioGroupValueChangeHandler, RadioOptions, useRadioGroup };
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./changes-DglRuvQa.js";import{a as t,n}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{i as r,t as i}from"./composite-CiKLX-Ti.js";import{n as a}from"./controllable-value-CDbRzlId.js";import{t as o}from"./form-reset-B7G3-0OT.js";import{createMixin as s,useBeforePaint as c,useId as l,useMemo as u,useStableEvent as d}from"@bgub/fig";import{on as f}from"@bgub/fig-dom";const p=s((e,t)=>({"aria-orientation":t.orientation,"aria-readonly":t.readOnly?`true`:void 0,bind:n(e,t.registry,(e,n)=>t.registry.bindContainer(e,n)),"data-disabled":t.disabled?``:void 0,"data-orientation":t.orientation,"data-readonly":t.readOnly?``:void 0,mix:[f(`click`,e=>{t.readOnly&&t.registry.itemAt(e.target)!==void 0&&e.preventDefault()}),f(`change`,e=>{let n=t.registry.itemAt(e.target);n!==void 0&&t.select(n.value,e,n.node)})],role:`radiogroup`})),m=s((e,t,r)=>{let i=r.disabled||e.props.disabled===!0;return{bind:n(e,t.registry,(e,n)=>{t.registry.bindItem(e,n,r.value,i),t.bindFormReset(e,n)}),checked:r.checked,"data-checked":r.checked?``:void 0,"data-disabled":i?``:void 0,"data-readonly":t.readOnly?``:void 0,disabled:i?!0:void 0,name:e.props.name??t.name,required:t.required?!0:e.props.required,type:`radio`,value:e.props.value??String(r.value)}});function h(n={}){let{disabled:s=!1,orientation:f=`vertical`,readOnly:h=!1,required:g=!1}=n,_=t(),v=a({value:n.value,defaultValue:n.defaultValue??null,onChange:n.onValueChange,equal:r,reconcile:_}),y=v.value,b=u(()=>i({container:`[role="radiogroup"]`,item:`input[type="radio"]`,name:`radio group`}),[]);c(()=>{b.containerNode()});let x=u(()=>o(v.reset),[]),S=d((t,n,r)=>{if(n.defaultPrevented||s||r.matches(`:disabled`)||h){_();return}v.request(()=>t,e(n,r))}),C=l(),w=n.name??`${C}-radio-group`,T={bindFormReset:x.bind,disabled:s,name:w,orientation:f,readOnly:h,registry:b,required:g,select:S};return{name:w,radio:(e,t={})=>m(T,{checked:y!==null&&r(e,y),disabled:s||t.disabled===!0,value:e}),root:()=>p(T),value:y}}function g(e){return e.children(h(e))}export{g as RadioGroup,h as useRadioGroup};
2
+ //# sourceMappingURL=radio-group.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"radio-group.js","names":[],"sources":["../src/radio-group/parts.ts","../src/radio-group/radio-group.tsx"],"sourcesContent":["import { createMixin, type MixinContext } from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport type { Composite, Orientation } from \"../internal/composite.ts\";\nimport { expectHost } from \"../internal/diagnostics.ts\";\nimport { bindPart } from \"../internal/parts.ts\";\n\n/** Widget-level state every part reads. Built once per root render. */\nexport interface RadioGroupPartState {\n readonly bindFormReset: (node: HTMLElement, signal: AbortSignal) => void;\n readonly disabled: boolean;\n readonly name: string;\n readonly orientation: Orientation;\n readonly readOnly: boolean;\n readonly registry: Composite;\n readonly required: boolean;\n readonly select: (value: unknown, event: Event, trigger: Element) => void;\n}\n\ninterface RadioOwnState {\n readonly checked: boolean;\n readonly disabled: boolean;\n readonly value: unknown;\n}\n\nexport const radioGroupMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: RadioGroupPartState) => ({\n \"aria-orientation\": state.orientation,\n \"aria-readonly\": state.readOnly ? \"true\" : undefined,\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindContainer(node, signal),\n ),\n \"data-disabled\": state.disabled ? \"\" : undefined,\n \"data-orientation\": state.orientation,\n \"data-readonly\": state.readOnly ? \"\" : undefined,\n // Radios sharing a name give the browser the roving tab stop, arrow\n // movement on both axes, wrapping, skipping disabled, and Space. The\n // group only has to hear what the platform decided.\n mix: [\n on(\"click\", (event) => {\n if (\n state.readOnly &&\n state.registry.itemAt(event.target) !== undefined\n ) {\n event.preventDefault();\n }\n }),\n on(\"change\", (event) => {\n const radio = state.registry.itemAt(event.target);\n if (radio === undefined) return;\n state.select(radio.value, event, radio.node);\n }),\n ],\n role: \"radiogroup\",\n }),\n);\n\n/** Applies to a native `<input type=\"radio\">`, which owns the interaction. */\nexport const radioMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: RadioGroupPartState, own: RadioOwnState) => {\n expectHost(context, \"radio\", \"input\");\n const disabled = own.disabled || context.props.disabled === true;\n return {\n bind: bindPart(context, state.registry, (node, signal) => {\n state.registry.bindItem(node, signal, own.value, disabled);\n state.bindFormReset(node, signal);\n }),\n checked: own.checked,\n \"data-checked\": own.checked ? \"\" : undefined,\n \"data-disabled\": disabled ? \"\" : undefined,\n \"data-readonly\": state.readOnly ? \"\" : undefined,\n disabled: disabled ? true : undefined,\n name: context.props.name ?? state.name,\n required: state.required ? true : context.props.required,\n type: \"radio\",\n // The submitted value is the string form of the identity. Supply a\n // `value` prop when the two should differ.\n value: context.props.value ?? String(own.value),\n };\n },\n);\n","import { useControllableValue } from \"../internal/controllable-value.ts\";\nimport {\n type FigNode,\n type MixinDescriptor,\n useBeforePaint,\n useId,\n useMemo,\n useStableEvent,\n} from \"@bgub/fig\";\nimport {\n type ChangeDetails,\n createChangeDetails,\n} from \"../internal/changes.ts\";\nimport {\n createComposite,\n type Orientation,\n sameValue,\n} from \"../internal/composite.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\nimport { assertAccessibleName } from \"../internal/diagnostics.ts\";\nimport { createFormReset } from \"../internal/form-reset.ts\";\nimport { radioGroupMixin, radioMixin } from \"./parts.ts\";\n\nexport type RadioGroupOrientation = Orientation;\n\nexport type RadioGroupValueChangeDetails = ChangeDetails;\n\nexport type RadioGroupValueChangeHandler<Value = unknown> = (\n value: Value | null,\n details: RadioGroupValueChangeDetails,\n signal: AbortSignal,\n) => void;\n\nexport interface RadioOptions {\n disabled?: boolean;\n}\n\nexport interface RadioGroupOptions<Value = unknown> {\n defaultValue?: Value | null;\n /** Disables every radio in the group. */\n disabled?: boolean;\n /** The submitted field name. Generated when absent. */\n name?: string;\n onValueChange?: RadioGroupValueChangeHandler<Value>;\n /** Labels the group's axis. Defaults to `vertical`, the ARIA default. */\n orientation?: RadioGroupOrientation;\n /** Prevents user changes while keeping radios focusable and submitted. */\n readOnly?: boolean;\n /** Requires a choice before the form submits. */\n required?: boolean;\n value?: Value | null;\n}\n\nexport interface RadioGroupParts<Value = unknown> {\n /** The field name every radio shares. */\n readonly name: string;\n readonly value: Value | null;\n /** Applies to a native `<input type=\"radio\">`. */\n radio(value: Value, options?: RadioOptions): MixinDescriptor;\n root(): MixinDescriptor;\n}\n\nexport interface RadioGroupProps<\n Value = unknown,\n> extends RadioGroupOptions<Value> {\n children: (group: RadioGroupParts<Value>) => FigNode;\n}\n\n/**\n * Coordinates one radio group over native radio inputs.\n *\n * Radios sharing a name are already a group to the browser, which owns the\n * single tab stop, arrow movement on either axis, wrapping, skipping disabled\n * radios, Space, form submission, and validity. The widget owns the selected\n * value and reports changes, and adds no keyboard handling of its own.\n */\nexport function useRadioGroup<Value = unknown>(\n options: RadioGroupOptions<Value> = {},\n): RadioGroupParts<Value> {\n const {\n disabled = false,\n orientation = \"vertical\",\n readOnly = false,\n required = false,\n } = options;\n const requestReconcile = useRegistrationReconcile();\n const selection = useControllableValue<Value | null>({\n value: options.value,\n defaultValue: options.defaultValue ?? null,\n onChange: options.onValueChange,\n equal: sameValue,\n reconcile: requestReconcile,\n });\n const value = selection.value;\n // Registration only: nothing renders from it, so there is nothing to\n // reconcile when a descendant mounts a radio.\n const registry = useMemo(\n () =>\n createComposite({\n container: '[role=\"radiogroup\"]',\n item: 'input[type=\"radio\"]',\n name: \"radio group\",\n }),\n [],\n );\n useBeforePaint(() => {\n const container = registry.containerNode();\n if (container !== null) assertAccessibleName(container, \"radio group\");\n });\n\n const formReset = useMemo(() => createFormReset(selection.reset), []);\n const select = useStableEvent(\n (next: unknown, event: Event, trigger: Element) => {\n if (\n event.defaultPrevented ||\n disabled ||\n trigger.matches(\":disabled\") ||\n readOnly\n ) {\n requestReconcile();\n return;\n }\n selection.request(\n () => next as Value,\n createChangeDetails(event, trigger),\n );\n },\n );\n\n const id = useId();\n const name = options.name ?? `${id}-radio-group`;\n const state = {\n bindFormReset: formReset.bind,\n disabled,\n name,\n orientation,\n readOnly,\n registry,\n required,\n select,\n };\n\n return {\n name,\n radio: (radioValue, radioOptions = {}) =>\n radioMixin(state, {\n checked: value !== null && sameValue(radioValue, value),\n disabled: disabled || radioOptions.disabled === true,\n value: radioValue,\n }),\n root: () => radioGroupMixin(state),\n value,\n };\n}\n\n/**\n * {@link useRadioGroup} as a component, for a group that is not already a\n * component of its own. Selection re-renders this root rather than the caller.\n */\nexport function RadioGroup<Value = unknown>(\n props: RadioGroupProps<Value>,\n): FigNode {\n return props.children(useRadioGroup(props));\n}\n"],"mappings":"sZAwBA,MAAa,EAAkC,GAC5C,EAAuB,KAAgC,CACtD,mBAAoB,EAAM,YAC1B,gBAAiB,EAAM,SAAW,OAAS,IAAA,GAC3C,KAAM,EAAS,EAAS,EAAM,UAAW,EAAM,IAC7C,EAAM,SAAS,cAAc,EAAM,CAAM,CAC3C,EACA,gBAAiB,EAAM,SAAW,GAAK,IAAA,GACvC,mBAAoB,EAAM,YAC1B,gBAAiB,EAAM,SAAW,GAAK,IAAA,GAIvC,IAAK,CACH,EAAG,QAAU,GAAU,CAEnB,EAAM,UACN,EAAM,SAAS,OAAO,EAAM,MAAM,IAAM,IAAA,IAExC,EAAM,eAAe,CAEzB,CAAC,EACD,EAAG,SAAW,GAAU,CACtB,IAAM,EAAQ,EAAM,SAAS,OAAO,EAAM,MAAM,EAC5C,IAAU,IAAA,IACd,EAAM,OAAO,EAAM,MAAO,EAAO,EAAM,IAAI,CAC7C,CAAC,CACH,EACA,KAAM,YACR,EACF,EAGa,EAA6B,GACvC,EAAuB,EAA4B,IAAuB,CAEzE,IAAM,EAAW,EAAI,UAAY,EAAQ,MAAM,WAAa,GAC5D,MAAO,CACL,KAAM,EAAS,EAAS,EAAM,UAAW,EAAM,IAAW,CACxD,EAAM,SAAS,SAAS,EAAM,EAAQ,EAAI,MAAO,CAAQ,EACzD,EAAM,cAAc,EAAM,CAAM,CAClC,CAAC,EACD,QAAS,EAAI,QACb,eAAgB,EAAI,QAAU,GAAK,IAAA,GACnC,gBAAiB,EAAW,GAAK,IAAA,GACjC,gBAAiB,EAAM,SAAW,GAAK,IAAA,GACvC,SAAU,EAAW,GAAO,IAAA,GAC5B,KAAM,EAAQ,MAAM,MAAQ,EAAM,KAClC,SAAU,EAAM,SAAW,GAAO,EAAQ,MAAM,SAChD,KAAM,QAGN,MAAO,EAAQ,MAAM,OAAS,OAAO,EAAI,KAAK,CAChD,CACF,CACF,ECHA,SAAgB,EACd,EAAoC,CAAC,EACb,CACxB,GAAM,CACJ,WAAW,GACX,cAAc,WACd,WAAW,GACX,WAAW,IACT,EACE,EAAmB,EAAyB,EAC5C,EAAY,EAAmC,CACnD,MAAO,EAAQ,MACf,aAAc,EAAQ,cAAgB,KACtC,SAAU,EAAQ,cAClB,MAAO,EACP,UAAW,CACb,CAAC,EACK,EAAQ,EAAU,MAGlB,EAAW,MAEb,EAAgB,CACd,UAAW,sBACX,KAAM,sBACN,KAAM,aACR,CAAC,EACH,CAAC,CACH,EACA,MAAqB,CACD,EAAS,cAAc,CAE3C,CAAC,EAED,IAAM,EAAY,MAAc,EAAgB,EAAU,KAAK,EAAG,CAAC,CAAC,EAC9D,EAAS,GACZ,EAAe,EAAc,IAAqB,CACjD,GACE,EAAM,kBACN,GACA,EAAQ,QAAQ,WAAW,GAC3B,EACA,CACA,EAAiB,EACjB,MACF,CACA,EAAU,YACF,EACN,EAAoB,EAAO,CAAO,CACpC,CACF,CACF,EAEM,EAAK,EAAM,EACX,EAAO,EAAQ,MAAQ,GAAG,EAAG,cAC7B,EAAQ,CACZ,cAAe,EAAU,KACzB,WACA,OACA,cACA,WACA,WACA,WACA,QACF,EAEA,MAAO,CACL,OACA,OAAQ,EAAY,EAAe,CAAC,IAClC,EAAW,EAAO,CAChB,QAAS,IAAU,MAAQ,EAAU,EAAY,CAAK,EACtD,SAAU,GAAY,EAAa,WAAa,GAChD,MAAO,CACT,CAAC,EACH,SAAY,EAAgB,CAAK,EACjC,OACF,CACF,CAMA,SAAgB,EACd,EACS,CACT,OAAO,EAAM,SAAS,EAAc,CAAK,CAAC,CAC5C"}
@@ -0,0 +1,2 @@
1
+ import{o as e,r as t}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{i as n}from"./composite-CiKLX-Ti.js";function r(r,i){let a=e(i);function o(e,t,n){a.bind(e,t,n),a.items()}function s(){let e=r.items(),i=a.items();for(let r of e){let e=i.find(e=>n(e.value,r.value));t(r.node,`aria-controls`,e?.node.id)}for(let r of i){let i=e.find(e=>n(e.value,r.value));i?.node,t(r.node,`aria-labelledby`,i?.node.id)}}return{bindPanel:o,sync:s}}export{r as t};
2
+ //# sourceMappingURL=relations-uMHtgW03.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations-uMHtgW03.js","names":[],"sources":["../src/internal/relations.ts"],"sourcesContent":["import { type Composite, sameValue } from \"./composite.ts\";\nimport { assertPanelOwner, assertUniqueValues } from \"./diagnostics.ts\";\nimport { setIdReference } from \"./parts.ts\";\nimport { createPartCollection } from \"./registration.ts\";\n\n/**\n * Pairs composite items with the panels they control.\n *\n * Generated ids cover the ordinary case at render time, but a caller may put\n * its own id on either host, so the committed DOM is the only place the real\n * pairing can be resolved.\n */\nexport function createRelations(\n composite: Composite,\n registrationChanged: () => void,\n) {\n const panels = createPartCollection<unknown>(registrationChanged);\n\n function bindPanel(\n node: HTMLElement,\n signal: AbortSignal,\n value: unknown,\n ): void {\n panels.bind(node, signal, value);\n assertUniqueValues(panels.items(), \"panel\");\n }\n\n function sync(): void {\n const items = composite.items();\n const mountedPanels = panels.items();\n assertUniqueValues(mountedPanels, \"panel\");\n for (const item of items) {\n const panel = mountedPanels.find((entry) =>\n sameValue(entry.value, item.value),\n );\n setIdReference(item.node, \"aria-controls\", panel?.node.id);\n }\n for (const panel of mountedPanels) {\n const owner = items.find((item) => sameValue(item.value, panel.value));\n assertPanelOwner(owner?.node);\n setIdReference(panel.node, \"aria-labelledby\", owner?.node.id);\n }\n }\n\n return { bindPanel, sync };\n}\n"],"mappings":"6HAYA,SAAgB,EACd,EACA,EACA,CACA,IAAM,EAAS,EAA8B,CAAmB,EAEhE,SAAS,EACP,EACA,EACA,EACM,CACN,EAAO,KAAK,EAAM,EAAQ,CAAK,EACZ,EAAO,MAAM,CAClC,CAEA,SAAS,GAAa,CACpB,IAAM,EAAQ,EAAU,MAAM,EACxB,EAAgB,EAAO,MAAM,EAEnC,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAQ,EAAc,KAAM,GAChC,EAAU,EAAM,MAAO,EAAK,KAAK,CACnC,EACA,EAAe,EAAK,KAAM,gBAAiB,GAAO,KAAK,EAAE,CAC3D,CACA,IAAK,IAAM,KAAS,EAAe,CACjC,IAAM,EAAQ,EAAM,KAAM,GAAS,EAAU,EAAK,MAAO,EAAM,KAAK,CAAC,EACpD,GAAO,KACxB,EAAe,EAAM,KAAM,kBAAmB,GAAO,KAAK,EAAE,CAC9D,CACF,CAEA,MAAO,CAAE,YAAW,MAAK,CAC3B"}
@@ -0,0 +1,45 @@
1
+ import { t as ChangeDetails } from "./changes--uvVfvy8.js";
2
+ import { n as PopoverOpenChangeDetails, r as PopoverOpenChangeHandler } from "./popover-CSPJNmHy.js";
3
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
4
+ //#region src/select/select.d.ts
5
+ type SelectOpenChangeDetails = PopoverOpenChangeDetails;
6
+ type SelectOpenChangeHandler = PopoverOpenChangeHandler;
7
+ type SelectValueChangeDetails = ChangeDetails;
8
+ type SelectValueChangeHandler<Value = unknown> = (value: Value | null, details: SelectValueChangeDetails, signal: AbortSignal) => void;
9
+ interface SelectOptionOptions {
10
+ disabled?: boolean;
11
+ /** Text used for typeahead when it differs from the host's text content. */
12
+ textValue?: string;
13
+ }
14
+ interface SelectOptions<Value = unknown> {
15
+ defaultOpen?: boolean;
16
+ defaultValue?: Value | null;
17
+ disabled?: boolean;
18
+ /** Converts the selected identity to its submitted value. Defaults to String. */
19
+ getFormValue?: (value: Value) => string;
20
+ id?: string;
21
+ name?: string;
22
+ onOpenChange?: SelectOpenChangeHandler;
23
+ onValueChange?: SelectValueChangeHandler<Value>;
24
+ open?: boolean;
25
+ readOnly?: boolean;
26
+ value?: Value | null;
27
+ }
28
+ interface SelectParts<Value = unknown> {
29
+ readonly open: boolean;
30
+ readonly value: Value | null;
31
+ hiddenInput(): MixinDescriptor;
32
+ option(value: Value, options?: SelectOptionOptions): MixinDescriptor;
33
+ popup(): MixinDescriptor;
34
+ setOpen(open: boolean): void;
35
+ trigger(): MixinDescriptor;
36
+ }
37
+ interface SelectProps<Value = unknown> extends SelectOptions<Value> {
38
+ children: (select: SelectParts<Value>) => FigNode;
39
+ }
40
+ /** Coordinates a custom select whose trigger, popup, and options are authored. */
41
+ declare function useSelect<Value = unknown>(options?: SelectOptions<Value>): SelectParts<Value>;
42
+ /** {@link useSelect} as a render-callback component. */
43
+ declare function Select<Value = unknown>(props: SelectProps<Value>): FigNode;
44
+ //#endregion
45
+ export { Select, SelectOpenChangeDetails, SelectOpenChangeHandler, SelectOptionOptions, SelectOptions, SelectParts, SelectProps, SelectValueChangeDetails, SelectValueChangeHandler, useSelect };
package/dist/select.js ADDED
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./changes-DglRuvQa.js";import{a as t,i as n,n as r,r as i,s as a}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{i as o}from"./composite-CiKLX-Ti.js";import{n as s}from"./controllable-value-CDbRzlId.js";import{t as c}from"./ids-Cvu36mFL.js";import{t as l}from"./form-reset-B7G3-0OT.js";import{t as u}from"./part-reference-B7zINPUK.js";import{t as d}from"./listbox-DcD6dHSg.js";import{n as f}from"./popover-UfJHN1h2.js";import{createMixin as p,useBeforePaint as m,useId as h,useMemo as g,useStableEvent as _,useState as v}from"@bgub/fig";import{on as y}from"@bgub/fig-dom";const b=p((e,t)=>{let i=t.disabled||e.props.disabled===!0;return{...n(e,{disabled:i,id:t.triggerId}),"aria-activedescendant":t.open&&t.highlighted!==null?t.registry.option(t.highlighted)?.node.id??t.idFor(t.highlighted,`option`):void 0,"aria-haspopup":`listbox`,"aria-readonly":t.readOnly?`true`:void 0,"data-readonly":t.readOnly?``:void 0,disabled:i?!0:void 0,bind:r(e,t.registry,t.bindTrigger),mix:y(`keydown`,e=>{if(e.defaultPrevented||i||e.isComposing||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey)return;let n=t.registry.move(t.highlighted,e.key);if(n!==void 0){e.preventDefault(),t.setHighlighted(n.value,!0),t.open||t.popover.setOpen(!0);return}if(e.key===`Enter`||e.key===` `){if(e.preventDefault(),!t.open){t.popover.setOpen(!0);return}let n=t.registry.option(t.highlighted);n!==void 0&&!t.readOnly&&t.select(n,e);return}let r=t.registry.typeahead(t.highlighted,e.key);r!==void 0&&(t.setHighlighted(r.value,!0),!t.open&&!t.readOnly&&t.select(r,e))}),role:`combobox`}}),x=p((e,t,n)=>[n,b(t)]),S=p((e,t)=>{let n=u(e,`aria-labelledby`,t.triggerId);return{...n.props,bind:r(e,t.registry,(e,r)=>{t.labels.set(e,n),t.registry.bindContainer(e,r)}),mix:[y(`pointerdown`,e=>{t.registry.optionAt(e.target)!==void 0&&e.preventDefault()}),y(`click`,e=>{if(e.defaultPrevented)return;let n=t.registry.optionAt(e.target);n!==void 0&&(n.disabled?e.preventDefault():e.button===0&&t.select(n,e))}),y(`pointermove`,e=>{if(e.pointerType===`touch`)return;let n=t.registry.optionAt(e.target);n!==void 0&&!n.disabled&&t.setHighlighted(n.value,!1)})],role:`listbox`}}),C=p((e,t,n)=>[n,S(t)]),w=p((e,t,n)=>{let i=n.disabled||e.props.disabled===!0;return{"aria-disabled":i?`true`:void 0,"aria-selected":n.selected?`true`:`false`,bind:r(e,t.registry,(e,r)=>t.registry.bindOption(e,r,{disabled:i,textValue:n.textValue,value:n.value})),"data-disabled":i?``:void 0,"data-highlighted":o(t.highlighted,n.value)?``:void 0,"data-selected":n.selected?``:void 0,id:e.props.id??t.idFor(n.value,`option`),role:`option`,type:e.type===`button`?e.props.type??`button`:void 0,tabindex:-1}}),T=p((e,t)=>({disabled:t.disabled?!0:void 0,name:e.props.name??t.name,type:`hidden`,value:e.props.value??t.formValue,bind:r(e,t.registry,t.bindHiddenInput)}));function E(n={}){let{disabled:r=!1,readOnly:u=!1}=n,p=n.value!==void 0,y=n.defaultValue!==void 0,b=t(),S=s({value:n.value,defaultValue:n.defaultValue??null,onChange:n.onValueChange,equal:o,reconcile:b}),E=S.value,[D,O]=v(()=>({value:E})),k=g(()=>d(`select`,b),[]),A=f({defaultOpen:n.defaultOpen,id:n.id,onOpenChange:n.onOpenChange,open:n.open}),j=g(()=>new WeakMap,[]),M=g(()=>a(b),[]),N=g(()=>l(S.reset),[]),P=_((t,n)=>{r||u||t.disabled||M.node()?.matches(`:disabled`)||S.request(()=>t.value,e(n,t.node))&&A.setOpen(!1)}),F=g(()=>({open:!1}),[]),I=_((e,t)=>{t!==void 0&&(F.scrollTo=t?{value:e}:void 0),o(D.value,e)?t&&b():O({value:e})});m(t=>{let n=A.open&&!F.open;F.open=A.open;let r=k.options();if(!p&&r.length>0){let t=E===null?void 0:k.option(E);if(E!==null&&t===void 0||E===null&&!y){let t=r.find(e=>!e.disabled),n=t===void 0?null:t.value;if(!o(n,E)){let t=e(null);S.request(()=>n,t)&&I(n,void 0)}}}if(A.open){let e=k.option(D.value)?.disabled===!1?D.value:k.option(E)?.disabled===!1?E:r.find(e=>!e.disabled)?.value??null;I(e,void 0),n&&F.scrollTo===void 0&&(F.scrollTo={value:e});let i=F.scrollTo;i!==void 0&&o(i.value,e)?queueMicrotask(()=>{t.aborted||F.scrollTo!==i||(F.scrollTo=void 0,k.option(e)?.node.scrollIntoView?.({block:`nearest`,inline:`nearest`}))}):F.scrollTo=void 0}else F.scrollTo=void 0;let a=M.node(),s=k.containerNode();if(a!==null&&s!==null){j.get(s)?.sync(s,a.id);let e=A.open&&D.value!==null?k.option(D.value)?.node.id??R(D.value,`option`):void 0;i(a,`aria-activedescendant`,e)}});let L=`${h()}-trigger`,R=c(),z=n.getFormValue??String,B={labels:j,bindHiddenInput:N.bind,bindTrigger:M.bind,disabled:r,formValue:E===null?``:z(E),highlighted:D.value,idFor:R,name:n.name,open:A.open,popover:A,readOnly:u,registry:k,select:P,setHighlighted:I,triggerId:L};return{hiddenInput:()=>T(B),open:A.open,option:(e,t={})=>w(B,{disabled:t.disabled===!0,selected:E!==null&&o(E,e),textValue:t.textValue,value:e}),popup:()=>C(B,A.popover()),setOpen:e=>A.setOpen(e),trigger:()=>x(B,A.trigger()),value:E}}function D(e){return e.children(E(e))}export{D as Select,E as useSelect};
2
+ //# sourceMappingURL=select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.js","names":[],"sources":["../src/select/select.tsx"],"sourcesContent":["import {\n createPartReference,\n type PartReference,\n} from \"../internal/part-reference.ts\";\nimport {\n createMixin,\n type FigNode,\n type MixinContext,\n type MixinDescriptor,\n useBeforePaint,\n useId,\n useMemo,\n useStableEvent,\n useState,\n} from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport {\n type ChangeDetails,\n createChangeDetails,\n} from \"../internal/changes.ts\";\nimport { useControllableValue } from \"../internal/controllable-value.ts\";\nimport { sameValue } from \"../internal/composite.ts\";\nimport { expectHost } from \"../internal/diagnostics.ts\";\nimport { createFormReset } from \"../internal/form-reset.ts\";\nimport { usePartIds } from \"../internal/ids.ts\";\nimport { createListbox, type ListboxOption } from \"../internal/listbox.ts\";\nimport { bindPart, setIdReference, triggerProps } from \"../internal/parts.ts\";\nimport { createPartSlot } from \"../internal/registration.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\nimport type {\n PopoverOpenChangeDetails,\n PopoverOpenChangeHandler,\n PopoverParts,\n} from \"../popover/popover.tsx\";\nimport { usePopover } from \"../popover/popover.tsx\";\n\nexport type SelectOpenChangeDetails = PopoverOpenChangeDetails;\nexport type SelectOpenChangeHandler = PopoverOpenChangeHandler;\nexport type SelectValueChangeDetails = ChangeDetails;\n\nexport type SelectValueChangeHandler<Value = unknown> = (\n value: Value | null,\n details: SelectValueChangeDetails,\n signal: AbortSignal,\n) => void;\n\nexport interface SelectOptionOptions {\n disabled?: boolean;\n /** Text used for typeahead when it differs from the host's text content. */\n textValue?: string;\n}\n\nexport interface SelectOptions<Value = unknown> {\n defaultOpen?: boolean;\n defaultValue?: Value | null;\n disabled?: boolean;\n /** Converts the selected identity to its submitted value. Defaults to String. */\n getFormValue?: (value: Value) => string;\n id?: string;\n name?: string;\n onOpenChange?: SelectOpenChangeHandler;\n onValueChange?: SelectValueChangeHandler<Value>;\n open?: boolean;\n readOnly?: boolean;\n value?: Value | null;\n}\n\nexport interface SelectParts<Value = unknown> {\n readonly open: boolean;\n readonly value: Value | null;\n hiddenInput(): MixinDescriptor;\n option(value: Value, options?: SelectOptionOptions): MixinDescriptor;\n popup(): MixinDescriptor;\n setOpen(open: boolean): void;\n trigger(): MixinDescriptor;\n}\n\nexport interface SelectProps<Value = unknown> extends SelectOptions<Value> {\n children: (select: SelectParts<Value>) => FigNode;\n}\n\ntype SelectRegistry = ReturnType<typeof createListbox>;\n\ninterface SelectState {\n readonly labels: WeakMap<HTMLElement, PartReference>;\n readonly bindHiddenInput: (node: HTMLElement, signal: AbortSignal) => void;\n readonly bindTrigger: (node: HTMLElement, signal: AbortSignal) => void;\n readonly disabled: boolean;\n readonly formValue: string;\n readonly highlighted: unknown;\n readonly idFor: (value: unknown, part: string) => string;\n readonly name: string | undefined;\n readonly open: boolean;\n readonly popover: PopoverParts;\n readonly readOnly: boolean;\n readonly registry: SelectRegistry;\n readonly select: (option: ListboxOption, event: Event) => void;\n readonly setHighlighted: (value: unknown, scroll: boolean) => void;\n readonly triggerId: string;\n}\n\ninterface SelectOptionState {\n readonly disabled: boolean;\n readonly selected: boolean;\n readonly textValue: string | undefined;\n readonly value: unknown;\n}\n\nconst selectTriggerBehavior = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: SelectState) => {\n const disabled = state.disabled || context.props.disabled === true;\n return {\n ...triggerProps(context, { disabled, id: state.triggerId }),\n \"aria-activedescendant\":\n state.open && state.highlighted !== null\n ? (state.registry.option(state.highlighted)?.node.id ??\n state.idFor(state.highlighted, \"option\"))\n : undefined,\n \"aria-haspopup\": \"listbox\",\n \"aria-readonly\": state.readOnly ? \"true\" : undefined,\n \"data-readonly\": state.readOnly ? \"\" : undefined,\n disabled: disabled ? true : undefined,\n bind: bindPart(context, state.registry, state.bindTrigger),\n mix: on(\"keydown\", (event) => {\n if (event.defaultPrevented) return;\n if (\n disabled ||\n event.isComposing ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey\n ) {\n return;\n }\n const moved = state.registry.move(state.highlighted, event.key);\n if (moved !== undefined) {\n event.preventDefault();\n state.setHighlighted(moved.value, true);\n if (!state.open) state.popover.setOpen(true);\n return;\n }\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n if (!state.open) {\n state.popover.setOpen(true);\n return;\n }\n const highlighted = state.registry.option(state.highlighted);\n if (highlighted !== undefined && !state.readOnly) {\n state.select(highlighted, event);\n }\n return;\n }\n const match = state.registry.typeahead(state.highlighted, event.key);\n if (match === undefined) return;\n state.setHighlighted(match.value, true);\n if (!state.open && !state.readOnly) state.select(match, event);\n }),\n role: \"combobox\",\n };\n },\n);\n\nconst selectTriggerMixin = /* @__PURE__ */ createMixin(\n (\n _context: MixinContext,\n state: SelectState,\n popoverTrigger: MixinDescriptor,\n ) => [popoverTrigger, selectTriggerBehavior(state)],\n);\n\nconst selectPopupBehavior = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: SelectState) => {\n const label = createPartReference(\n context,\n \"aria-labelledby\",\n state.triggerId,\n );\n return {\n ...label.props,\n bind: bindPart(context, state.registry, (node, signal) => {\n state.labels.set(node, label);\n state.registry.bindContainer(node, signal);\n }),\n mix: [\n on(\"pointerdown\", (event) => {\n if (state.registry.optionAt(event.target) !== undefined) {\n event.preventDefault();\n }\n }),\n on(\"click\", (event) => {\n if (event.defaultPrevented) return;\n const option = state.registry.optionAt(event.target);\n if (option === undefined) return;\n if (option.disabled) event.preventDefault();\n else if (event.button === 0) state.select(option, event);\n }),\n on(\"pointermove\", (event) => {\n if (event.pointerType === \"touch\") return;\n const option = state.registry.optionAt(event.target);\n if (option !== undefined && !option.disabled) {\n state.setHighlighted(option.value, false);\n }\n }),\n ],\n role: \"listbox\",\n };\n },\n);\n\nconst selectPopupMixin = /* @__PURE__ */ createMixin(\n (\n _context: MixinContext,\n state: SelectState,\n popoverPopup: MixinDescriptor,\n ) => [popoverPopup, selectPopupBehavior(state)],\n);\n\nconst selectOptionMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: SelectState, own: SelectOptionState) => {\n const disabled = own.disabled || context.props.disabled === true;\n return {\n \"aria-disabled\": disabled ? \"true\" : undefined,\n \"aria-selected\": own.selected ? \"true\" : \"false\",\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindOption(node, signal, {\n disabled,\n textValue: own.textValue,\n value: own.value,\n }),\n ),\n \"data-disabled\": disabled ? \"\" : undefined,\n \"data-highlighted\": sameValue(state.highlighted, own.value)\n ? \"\"\n : undefined,\n \"data-selected\": own.selected ? \"\" : undefined,\n id: context.props.id ?? state.idFor(own.value, \"option\"),\n role: \"option\",\n type:\n context.type === \"button\"\n ? (context.props.type ?? \"button\")\n : undefined,\n tabindex: -1,\n };\n },\n);\n\nconst selectHiddenInputMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: SelectState) => {\n expectHost(context, \"select hidden input\", \"input\");\n return {\n disabled: state.disabled ? true : undefined,\n name: context.props.name ?? state.name,\n type: \"hidden\",\n value: context.props.value ?? state.formValue,\n bind: bindPart(context, state.registry, state.bindHiddenInput),\n };\n },\n);\n\n/** Coordinates a custom select whose trigger, popup, and options are authored. */\nexport function useSelect<Value = unknown>(\n options: SelectOptions<Value> = {},\n): SelectParts<Value> {\n const { disabled = false, readOnly = false } = options;\n const controlled = options.value !== undefined;\n const explicitDefault = options.defaultValue !== undefined;\n const registrationChanged = useRegistrationReconcile();\n const selection = useControllableValue<Value | null>({\n value: options.value,\n defaultValue: options.defaultValue ?? null,\n onChange: options.onValueChange,\n equal: sameValue,\n reconcile: registrationChanged,\n });\n const value = selection.value;\n const [highlighted, setHighlightedState] = useState<{\n readonly value: unknown;\n }>(() => ({ value }));\n const registry = useMemo(\n () => createListbox(\"select\", registrationChanged),\n [],\n );\n const popover = usePopover({\n defaultOpen: options.defaultOpen,\n id: options.id,\n onOpenChange: options.onOpenChange,\n open: options.open,\n });\n const labels = useMemo(() => new WeakMap<HTMLElement, PartReference>(), []);\n const trigger = useMemo(() => createPartSlot(registrationChanged), []);\n const formReset = useMemo(() => createFormReset(selection.reset), []);\n const select = useStableEvent((option: ListboxOption, event: Event) => {\n if (\n disabled ||\n readOnly ||\n option.disabled ||\n trigger.node()?.matches(\":disabled\")\n )\n return;\n if (\n !selection.request(\n () => option.value as Value,\n createChangeDetails(event, option.node),\n )\n )\n return;\n popover.setOpen(false);\n });\n const navigation = useMemo<{ open: boolean; scrollTo?: { value: unknown } }>(\n () => ({ open: false }),\n [],\n );\n const setHighlighted = useStableEvent(\n (next: unknown, scroll: boolean | undefined) => {\n if (scroll !== undefined)\n navigation.scrollTo = scroll ? { value: next } : undefined;\n if (!sameValue(highlighted.value, next)) {\n setHighlightedState({ value: next });\n } else if (scroll) registrationChanged();\n },\n );\n\n useBeforePaint((signal) => {\n const opening = popover.open && !navigation.open;\n navigation.open = popover.open;\n const mounted = registry.options();\n if (!controlled && mounted.length > 0) {\n const selected = value === null ? undefined : registry.option(value);\n const shouldRepair =\n (value !== null && selected === undefined) ||\n (value === null && !explicitDefault);\n if (shouldRepair) {\n const fallback = mounted.find((entry) => !entry.disabled);\n const next = fallback === undefined ? null : fallback.value;\n if (!sameValue(next, value)) {\n const details = createChangeDetails(null);\n if (selection.request(() => next as Value | null, details)) {\n setHighlighted(next, undefined);\n }\n }\n }\n }\n if (popover.open) {\n const next =\n registry.option(highlighted.value)?.disabled === false\n ? highlighted.value\n : registry.option(value)?.disabled === false\n ? value\n : (mounted.find((entry) => !entry.disabled)?.value ?? null);\n setHighlighted(next, undefined);\n // Wait until every layout hook (including optional positioning) has\n // applied its size constraints. Keep intent across interrupted commits.\n if (opening && navigation.scrollTo === undefined)\n navigation.scrollTo = { value: next };\n const scrollTo = navigation.scrollTo;\n if (scrollTo !== undefined && sameValue(scrollTo.value, next)) {\n queueMicrotask(() => {\n if (signal.aborted || navigation.scrollTo !== scrollTo) return;\n navigation.scrollTo = undefined;\n registry\n .option(next)\n ?.node.scrollIntoView?.({ block: \"nearest\", inline: \"nearest\" });\n });\n } else navigation.scrollTo = undefined;\n } else navigation.scrollTo = undefined;\n const triggerNode = trigger.node();\n const popupNode = registry.containerNode();\n if (triggerNode !== null && popupNode !== null) {\n labels.get(popupNode)?.sync(popupNode, triggerNode.id);\n const optionId =\n popover.open && highlighted.value !== null\n ? (registry.option(highlighted.value)?.node.id ??\n idFor(highlighted.value, \"option\"))\n : undefined;\n setIdReference(triggerNode, \"aria-activedescendant\", optionId);\n }\n });\n\n const id = useId();\n const triggerId = `${id}-trigger`;\n const idFor = usePartIds();\n const getFormValue = options.getFormValue ?? String;\n const state: SelectState = {\n labels,\n bindHiddenInput: formReset.bind,\n bindTrigger: trigger.bind,\n disabled,\n formValue: value === null ? \"\" : getFormValue(value),\n highlighted: highlighted.value,\n idFor,\n name: options.name,\n open: popover.open,\n popover,\n readOnly,\n registry,\n select,\n setHighlighted,\n triggerId,\n };\n\n return {\n hiddenInput: () => selectHiddenInputMixin(state),\n open: popover.open,\n option: (optionValue, optionOptions = {}) =>\n selectOptionMixin(state, {\n disabled: optionOptions.disabled === true,\n selected: value !== null && sameValue(value, optionValue),\n textValue: optionOptions.textValue,\n value: optionValue,\n }),\n popup: () => selectPopupMixin(state, popover.popover()),\n setOpen: (open) => popover.setOpen(open),\n trigger: () => selectTriggerMixin(state, popover.trigger()),\n value,\n };\n}\n\n/** {@link useSelect} as a render-callback component. */\nexport function Select<Value = unknown>(props: SelectProps<Value>): FigNode {\n return props.children(useSelect(props));\n}\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { t as ChangeDetails } from "./changes--uvVfvy8.js";
2
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
3
+ //#region src/slider/slider.d.ts
4
+ type SliderValueChangeDetails = ChangeDetails;
5
+ type SliderValueCommitDetails = Pick<ChangeDetails, "event" | "trigger">;
6
+ type SliderValueChangeHandler = (value: number, details: SliderValueChangeDetails, signal: AbortSignal) => void;
7
+ type SliderValueCommitHandler = (value: number, details: SliderValueCommitDetails, signal: AbortSignal) => void;
8
+ interface SliderOptions {
9
+ value?: number;
10
+ /** Defaults to the native range midpoint, snapped to step. */
11
+ defaultValue?: number;
12
+ min?: number;
13
+ max?: number;
14
+ step?: number | "any";
15
+ disabled?: boolean;
16
+ readOnly?: boolean;
17
+ name?: string;
18
+ onValueChange?: SliderValueChangeHandler;
19
+ /** Reports a native change after accepted input; does not undo earlier input. */
20
+ onValueCommit?: SliderValueCommitHandler;
21
+ }
22
+ interface SliderParts {
23
+ readonly value: number;
24
+ /** Applies to one native input range. Label it with a label or ARIA. */
25
+ control(): MixinDescriptor;
26
+ setValue(value: number): void;
27
+ }
28
+ interface SliderProps extends SliderOptions {
29
+ children: (slider: SliderParts) => FigNode;
30
+ }
31
+ /** Single-thumb range using native pointer, keyboard, focus and form behavior. */
32
+ declare function useSlider(options?: SliderOptions): SliderParts;
33
+ declare function Slider(props: SliderProps): FigNode;
34
+ //#endregion
35
+ export { Slider, SliderOptions, SliderParts, SliderProps, SliderValueChangeDetails, SliderValueChangeHandler, SliderValueCommitDetails, SliderValueCommitHandler, useSlider };
package/dist/slider.js ADDED
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./changes-DglRuvQa.js";import{a as t,n,o as r}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{n as i}from"./controllable-value-CDbRzlId.js";import{t as a}from"./form-reset-B7G3-0OT.js";import{createMixin as o,useBeforePaint as s,useMemo as c,useStableEvent as l}from"@bgub/fig";import{on as u}from"@bgub/fig-dom";const d=new Set([`ArrowLeft`,`ArrowRight`,`ArrowUp`,`ArrowDown`,`Home`,`End`,`PageUp`,`PageDown`]),f=o((e,t)=>{let r=t.readOnly||e.props.readonly===!0;return{bind:n(e,t.owner,t.bind),type:`range`,min:t.min,max:t.max,step:t.step,value:t.value,name:e.props.name??t.name,disabled:t.disabled?!0:e.props.disabled,"data-disabled":t.disabled||e.props.disabled===!0?``:void 0,"data-readonly":r?``:void 0,"aria-readonly":r?`true`:void 0,readonly:r?!0:void 0,mix:[u(`keydown`,e=>{r&&d.has(e.key)&&e.preventDefault()}),u(`pointerdown`,e=>{!r||e.defaultPrevented||e.button!==0||(e.preventDefault(),e.currentTarget instanceof HTMLInputElement&&!e.currentTarget.matches(`:disabled`)&&e.currentTarget.focus({preventScroll:!0}))}),u(`input`,e=>{e.currentTarget instanceof HTMLInputElement&&t.input(e.currentTarget,e,!1)}),u(`change`,e=>{e.currentTarget instanceof HTMLInputElement&&t.input(e.currentTarget,e,!0)})]}});function p(n={}){let o=n.min!==void 0&&Number.isFinite(n.min)?n.min:0,u=n.max!==void 0&&Number.isFinite(n.max)?n.max:100,d=n.step===`any`?`any`:n.step!==void 0&&Number.isFinite(n.step)&&n.step>0?n.step:1,p=n.value!==void 0,m=t(),g=i({value:n.value,defaultValue:n.defaultValue,onChange:(e,t,r)=>{n.onValueChange?.(h(e,o,u,d),t,r)},equal:(e,t)=>h(e,o,u,d)===h(t,o,u,d),reconcile:m}),_=h(g.value,o,u,d),v=l(()=>h(g.current(),o,u,d)),y=c(()=>r(m),[]),b=c(()=>({pending:void 0}),[]),x=l((e,t,r)=>{n.onValueCommit?.(e,t,r)}),S=l((t,n,r)=>g.request(()=>t,e(n,r))),C=l((e,t,r)=>{if(t.defaultPrevented||e.matches(`:disabled`)||e.readOnly||n.readOnly){e.value=String(v()),r&&(b.pending=void 0);return}let i=h(e.valueAsNumber,o,u,d),a=i!==v(),s=r&&b.pending===i||S(i,t,e);s&&a&&(b.pending=i),s||(e.value=String(v())),(!s||p)&&m(),r&&(s&&b.pending===i?(b.pending=void 0,x(i,{event:t,trigger:e})):b.pending=void 0)}),w=l(()=>{b.pending=void 0,g.reset(),m()}),T=c(()=>a(w),[]);s(()=>{let e=y.items();for(let{node:t}of e);});let E={owner:y,value:_,min:o,max:u,step:d,disabled:n.disabled===!0,readOnly:n.readOnly===!0,name:n.name,bind:(e,t)=>{y.bind(e,t,void 0),T.bind(e,t)},input:C};return{value:_,control:()=>f(E),setValue:l(e=>{let t=h(e,o,u,d),n=t!==v();S(t,null)&&n&&(b.pending=void 0),p&&m()})}}function m(e){return e.children(p(e))}function h(e,t,n,r){if(n<=t)return t;let i=t/2+n/2,a=Math.max(t,Math.min(n,e!==void 0&&Number.isFinite(e)?e:i));if(r===`any`)return a;let o=a-t,s=Number.isFinite(o)?o/r:a/r-t/r;if(!Number.isSafeInteger(Math.floor(s)))return a;let c=Math.round(s+Math.min(1e-7,2**-52*Math.abs(s))),l=Math.min(r*1e-7,2**-52*Math.max(Math.abs(t),Math.abs(n),Math.abs(r))*2);_(t,r,c)-n>l&&--c;let u=_(t,r,c),d=Math.max(g(t),g(r)),f=d<=100?Number(u.toFixed(d)):u;return Math.max(t,Math.min(n,f))}function g(e){let[t=``,n=`0`]=String(e).split(`e`);return Math.max(0,(t.split(`.`)[1]?.length??0)-Number(n))}function _(e,t,n){let r=n*t;return Number.isFinite(r)?e+r:(e/t+n)*t}export{m as Slider,p as useSlider};
2
+ //# sourceMappingURL=slider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slider.js","names":[],"sources":["../src/slider/slider.tsx"],"sourcesContent":["import {\n createMixin,\n type FigNode,\n type MixinDescriptor,\n useBeforePaint,\n useMemo,\n useStableEvent,\n} from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport {\n type ChangeDetails,\n createChangeDetails,\n} from \"../internal/changes.ts\";\nimport {\n assertControlLabel,\n assertSinglePart,\n expectHost,\n} from \"../internal/diagnostics.ts\";\nimport { useControllableValue } from \"../internal/controllable-value.ts\";\nimport { createFormReset } from \"../internal/form-reset.ts\";\nimport { bindPart } from \"../internal/parts.ts\";\nimport { createPartCollection } from \"../internal/registration.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\n\nexport type SliderValueChangeDetails = ChangeDetails;\nexport type SliderValueCommitDetails = Pick<ChangeDetails, \"event\" | \"trigger\">;\nexport type SliderValueChangeHandler = (\n value: number,\n details: SliderValueChangeDetails,\n signal: AbortSignal,\n) => void;\nexport type SliderValueCommitHandler = (\n value: number,\n details: SliderValueCommitDetails,\n signal: AbortSignal,\n) => void;\n\nexport interface SliderOptions {\n value?: number;\n /** Defaults to the native range midpoint, snapped to step. */\n defaultValue?: number;\n min?: number;\n max?: number;\n step?: number | \"any\";\n disabled?: boolean;\n readOnly?: boolean;\n name?: string;\n onValueChange?: SliderValueChangeHandler;\n /** Reports a native change after accepted input; does not undo earlier input. */\n onValueCommit?: SliderValueCommitHandler;\n}\n\nexport interface SliderParts {\n readonly value: number;\n /** Applies to one native input range. Label it with a label or ARIA. */\n control(): MixinDescriptor;\n setValue(value: number): void;\n}\n\nexport interface SliderProps extends SliderOptions {\n children: (slider: SliderParts) => FigNode;\n}\n\ninterface SliderState {\n readonly owner: object;\n readonly value: number;\n readonly min: number;\n readonly max: number;\n readonly step: number | \"any\";\n readonly disabled: boolean;\n readonly readOnly: boolean;\n readonly name: string | undefined;\n readonly bind: (node: HTMLElement, signal: AbortSignal) => void;\n readonly input: (\n node: HTMLInputElement,\n event: Event,\n commit: boolean,\n ) => void;\n}\n\nconst mutatingKeys = new Set([\n \"ArrowLeft\",\n \"ArrowRight\",\n \"ArrowUp\",\n \"ArrowDown\",\n \"Home\",\n \"End\",\n \"PageUp\",\n \"PageDown\",\n]);\n\nconst sliderControl = /* @__PURE__ */ createMixin(\n (context, state: SliderState) => {\n expectHost(context, \"slider control\", \"input\");\n const readOnly = state.readOnly || context.props.readonly === true;\n return {\n bind: bindPart(context, state.owner, state.bind),\n type: \"range\",\n min: state.min,\n max: state.max,\n step: state.step,\n value: state.value,\n name: context.props.name ?? state.name,\n disabled: state.disabled ? true : context.props.disabled,\n \"data-disabled\":\n state.disabled || context.props.disabled === true ? \"\" : undefined,\n \"data-readonly\": readOnly ? \"\" : undefined,\n \"aria-readonly\": readOnly ? \"true\" : undefined,\n readonly: readOnly ? true : undefined,\n mix: [\n on(\"keydown\", (event) => {\n if (readOnly && mutatingKeys.has(event.key)) event.preventDefault();\n }),\n on(\"pointerdown\", (event) => {\n if (!readOnly || event.defaultPrevented || event.button !== 0) return;\n event.preventDefault();\n if (\n event.currentTarget instanceof HTMLInputElement &&\n !event.currentTarget.matches(\":disabled\")\n ) {\n event.currentTarget.focus({ preventScroll: true });\n }\n }),\n on(\"input\", (event) => {\n if (event.currentTarget instanceof HTMLInputElement)\n state.input(event.currentTarget, event, false);\n }),\n on(\"change\", (event) => {\n if (event.currentTarget instanceof HTMLInputElement)\n state.input(event.currentTarget, event, true);\n }),\n ],\n };\n },\n);\n\n/** Single-thumb range using native pointer, keyboard, focus and form behavior. */\nexport function useSlider(options: SliderOptions = {}): SliderParts {\n const min =\n options.min !== undefined && Number.isFinite(options.min) ? options.min : 0;\n const max =\n options.max !== undefined && Number.isFinite(options.max)\n ? options.max\n : 100;\n const step =\n options.step === \"any\"\n ? \"any\"\n : options.step !== undefined &&\n Number.isFinite(options.step) &&\n options.step > 0\n ? options.step\n : 1;\n const controlled = options.value !== undefined;\n const requestReconcile = useRegistrationReconcile();\n const stateValue = useControllableValue<number | undefined>({\n value: options.value,\n defaultValue: options.defaultValue,\n onChange: (next, details, signal) => {\n options.onValueChange?.(sanitize(next, min, max, step), details, signal);\n },\n equal: (left, right) =>\n sanitize(left, min, max, step) === sanitize(right, min, max, step),\n reconcile: requestReconcile,\n });\n const value = sanitize(stateValue.value, min, max, step);\n const currentValue = useStableEvent(() =>\n sanitize(stateValue.current(), min, max, step),\n );\n const inputs = useMemo(\n () => createPartCollection<undefined>(requestReconcile),\n [],\n );\n const tracker = useMemo(\n () => ({ pending: undefined as number | undefined }),\n [],\n );\n const emitCommit = useStableEvent(\n (next: number, details: SliderValueCommitDetails, signal: AbortSignal) => {\n options.onValueCommit?.(next, details, signal);\n },\n );\n const requestValue = useStableEvent(\n (next: number, event: Event | null, node?: HTMLInputElement): boolean => {\n return stateValue.request(() => next, createChangeDetails(event, node));\n },\n );\n const input = useStableEvent(\n (node: HTMLInputElement, event: Event, commit: boolean) => {\n if (\n event.defaultPrevented ||\n node.matches(\":disabled\") ||\n node.readOnly ||\n options.readOnly\n ) {\n node.value = String(currentValue());\n if (commit) tracker.pending = undefined;\n return;\n }\n const next = sanitize(node.valueAsNumber, min, max, step);\n const changed = next !== currentValue();\n // Native input and change may arrive before the owner rerenders.\n // The input request has already been accepted; committing it is not\n // a second value change.\n const accepted =\n (commit && tracker.pending === next) || requestValue(next, event, node);\n if (accepted && changed) tracker.pending = next;\n if (!accepted) node.value = String(currentValue());\n // Reconciliation restores refused controlled input after the owner has\n // had a chance to commit its update, without corrupting a native change\n // event following input in the same batch.\n if (!accepted || controlled) requestReconcile();\n if (commit) {\n if (accepted && tracker.pending === next) {\n tracker.pending = undefined;\n emitCommit(next, { event, trigger: node });\n } else tracker.pending = undefined;\n }\n },\n );\n const reset = useStableEvent(() => {\n tracker.pending = undefined;\n stateValue.reset();\n requestReconcile();\n });\n const formReset = useMemo(() => createFormReset(reset), []);\n useBeforePaint(() => {\n const controls = inputs.items();\n assertSinglePart(controls, \"slider control\");\n for (const { node } of controls) assertControlLabel(node);\n });\n\n const state: SliderState = {\n owner: inputs,\n value,\n min,\n max,\n step,\n disabled: options.disabled === true,\n readOnly: options.readOnly === true,\n name: options.name,\n bind: (node, signal) => {\n inputs.bind(node, signal, undefined);\n formReset.bind(node, signal);\n },\n input,\n };\n const setValue = useStableEvent((next: number) => {\n const normalized = sanitize(next, min, max, step);\n const changed = normalized !== currentValue();\n if (requestValue(normalized, null) && changed) tracker.pending = undefined;\n if (controlled) requestReconcile();\n });\n return { value, control: () => sliderControl(state), setValue };\n}\n\nexport function Slider(props: SliderProps): FigNode {\n return props.children(useSlider(props));\n}\n\n/** Native range normalization with an explicit min (also the step base). */\nfunction sanitize(\n value: number | undefined,\n min: number,\n max: number,\n step: number | \"any\",\n): number {\n if (max <= min) return min;\n const midpoint = min / 2 + max / 2;\n const finiteValue =\n value !== undefined && Number.isFinite(value) ? value : midpoint;\n const clamped = Math.max(min, Math.min(max, finiteValue));\n if (step === \"any\") return clamped;\n const distance = clamped - min;\n const steps = Number.isFinite(distance)\n ? distance / step\n : clamped / step - min / step;\n // Beyond safe integer precision the step is smaller than representable changes.\n if (!Number.isSafeInteger(Math.floor(steps))) return clamped;\n let count = Math.round(\n steps + Math.min(1e-7, Number.EPSILON * Math.abs(steps)),\n );\n const tolerance = Math.min(\n step * 1e-7,\n Number.EPSILON * Math.max(Math.abs(min), Math.abs(max), Math.abs(step)) * 2,\n );\n if (stepValue(min, step, count) - max > tolerance) count -= 1;\n const snapped = stepValue(min, step, count);\n // Remove decimal arithmetic noise without losing significant fractional digits.\n const places = Math.max(decimalPlaces(min), decimalPlaces(step));\n const rounded = places <= 100 ? Number(snapped.toFixed(places)) : snapped;\n return Math.max(min, Math.min(max, rounded));\n}\n\nfunction decimalPlaces(value: number): number {\n const [mantissa = \"\", exponent = \"0\"] = String(value).split(\"e\");\n return Math.max(0, (mantissa.split(\".\")[1]?.length ?? 0) - Number(exponent));\n}\n\n/** Avoid overflowing the intermediate product when min and max straddle zero. */\nfunction stepValue(min: number, step: number, count: number): number {\n const offset = count * step;\n return Number.isFinite(offset) ? min + offset : (min / step + count) * step;\n}\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { i as ToggleControlParts, n as CheckedChangeHandler, r as ToggleControlOptions, t as CheckedChangeDetails } from "./toggle-q6B-U4Nq.js";
2
+ import { FigNode } from "@bgub/fig";
3
+ //#region src/switch/switch.d.ts
4
+ type SwitchCheckedChangeDetails = CheckedChangeDetails;
5
+ type SwitchCheckedChangeHandler = CheckedChangeHandler;
6
+ /** A switch is on or off, so it has no indeterminate state. */
7
+ type SwitchOptions = Omit<ToggleControlOptions, "indeterminate">;
8
+ type SwitchParts = ToggleControlParts;
9
+ interface SwitchProps extends SwitchOptions {
10
+ children: (control: SwitchParts) => FigNode;
11
+ }
12
+ /**
13
+ * Coordinates one switch: a native checkbox carrying `role="switch"`, so the
14
+ * platform still owns toggling, focus, Space, and form submission while
15
+ * assistive technology announces it as on or off rather than checked.
16
+ */
17
+ declare function useSwitch(options?: SwitchOptions): SwitchParts;
18
+ /**
19
+ * {@link useSwitch} as a component, for a switch that is not already a
20
+ * component of its own.
21
+ */
22
+ declare function Switch(props: SwitchProps): FigNode;
23
+ //#endregion
24
+ export { Switch, SwitchCheckedChangeDetails, SwitchCheckedChangeHandler, SwitchOptions, SwitchParts, SwitchProps, useSwitch };
package/dist/switch.js ADDED
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./toggle-B0hOxshF.js";function t(t={}){return e(t,`switch`)}function n(e){return e.children(t(e))}export{n as Switch,t as useSwitch};
2
+ //# sourceMappingURL=switch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"switch.js","names":[],"sources":["../src/switch/switch.tsx"],"sourcesContent":["import type { FigNode } from \"@bgub/fig\";\nimport type {\n CheckedChangeDetails,\n CheckedChangeHandler,\n ToggleControlOptions,\n ToggleControlParts,\n} from \"../internal/toggle.ts\";\nimport { useToggleControl } from \"../internal/toggle.ts\";\n\nexport type SwitchCheckedChangeDetails = CheckedChangeDetails;\n\nexport type SwitchCheckedChangeHandler = CheckedChangeHandler;\n\n/** A switch is on or off, so it has no indeterminate state. */\nexport type SwitchOptions = Omit<ToggleControlOptions, \"indeterminate\">;\n\nexport type SwitchParts = ToggleControlParts;\n\nexport interface SwitchProps extends SwitchOptions {\n children: (control: SwitchParts) => FigNode;\n}\n\n/**\n * Coordinates one switch: a native checkbox carrying `role=\"switch\"`, so the\n * platform still owns toggling, focus, Space, and form submission while\n * assistive technology announces it as on or off rather than checked.\n */\nexport function useSwitch(options: SwitchOptions = {}): SwitchParts {\n return useToggleControl(options, \"switch\");\n}\n\n/**\n * {@link useSwitch} as a component, for a switch that is not already a\n * component of its own.\n */\nexport function Switch(props: SwitchProps): FigNode {\n return props.children(useSwitch(props));\n}\n"],"mappings":"yCA2BA,SAAgB,EAAU,EAAyB,CAAC,EAAgB,CAClE,OAAO,EAAiB,EAAS,QAAQ,CAC3C,CAMA,SAAgB,EAAO,EAA6B,CAClD,OAAO,EAAM,SAAS,EAAU,CAAK,CAAC,CACxC"}
@@ -0,0 +1,20 @@
1
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
2
+ //#region src/tabs/indicator.d.ts
3
+ interface TabsIndicatorParts {
4
+ indicator(): MixinDescriptor;
5
+ /** Composes with the matching `tabs.list()` descriptor. */
6
+ list(): MixinDescriptor;
7
+ }
8
+ interface TabsIndicatorProps {
9
+ children: (indicator: TabsIndicatorParts) => FigNode;
10
+ }
11
+ /**
12
+ * Optional visual behavior for a tabs widget. The core tabs entry owns only
13
+ * selection and accessibility; this hook measures the active tab and publishes
14
+ * its box as `--active-tab-*` CSS properties.
15
+ */
16
+ declare function useTabsIndicator(): TabsIndicatorParts;
17
+ /** {@link useTabsIndicator} as a render-callback component. */
18
+ declare function TabsIndicator(props: TabsIndicatorProps): FigNode;
19
+ //#endregion
20
+ export { TabsIndicator, TabsIndicatorParts, TabsIndicatorProps, useTabsIndicator };
@@ -0,0 +1,2 @@
1
+ import{a as e,n as t,s as n}from"../parts-I0jdefyp.js";import{createMixin as r,useBeforePaint as i,useMemo as a}from"@bgub/fig";const o=r((e,n)=>({bind:t(e,n,n.bindList)})),s=r((e,n)=>({bind:t(e,n,n.bindIndicator),hidden:!0,role:`presentation`}));function c(){let t=e(),n=a(()=>u(t),[]);return i(()=>{n.sync()}),{indicator:()=>s(n),list:()=>o(n)}}function l(e){return e.children(c())}function u(e){let t=n(e),r=n(e),i=null,a=[];function o(e,n){t.bind(e,n)&&n.addEventListener(`abort`,()=>{t.node()===null&&(i?.disconnect(),i=null)},{once:!0})}function s(){let e=r.node();if(t.node()===null){i?.disconnect(),i=null;return}if(c(),typeof ResizeObserver>`u`)return;i===null&&(i=new ResizeObserver(c),a=[]);let n=d(e),o=e===null?n:[e,...n];if(o.length!==a.length||o.some((e,t)=>e!==a[t])){i.disconnect();for(let e of o)i.observe(e);a=o}}function c(){let e=t.node();if(e===null)return;let n=r.node();p(e,n,d(n).find(e=>e.getAttribute(`aria-selected`)===`true`)??null)}return{bindIndicator:o,bindList:r.bind,sync:s}}function d(e){return e===null?[]:[...e.querySelectorAll(`[role="tab"]`)].filter(t=>t.closest(`[role="tablist"]`)===e)}const f=[`bottom`,`height`,`left`,`right`,`top`,`width`];function p(e,t,n){let r=e.style;if(t===null||n===null){e.hidden=!0;for(let e of f)r.removeProperty(`--active-tab-${e}`);return}let i=n.getBoundingClientRect(),a=t.getBoundingClientRect(),o=getComputedStyle(n),s=getComputedStyle(t),c=m(o,`width`,i.width),l=m(o,`height`,i.height),u=t.offsetWidth||m(s,`width`,a.width),d=t.offsetHeight||m(s,`height`,a.height),p=u>0?a.width/u:1,h=d>0?a.height/d:1,g=p?(i.left-a.left)/p+t.scrollLeft-t.clientLeft:n.offsetLeft,_=h?(i.top-a.top)/h+t.scrollTop-t.clientTop:n.offsetTop,v={bottom:t.scrollHeight-_-l,height:l,left:g,right:t.scrollWidth-g-c,top:_,width:c};for(let e of f)r.setProperty(`--active-tab-${e}`,`${v[e]}px`);e.hidden=!(c>0&&l>0)}function m(e,t,n){let r=parseFloat(e[t]);return Number.isFinite(r)?e.boxSizing===`border-box`?r:(t===`width`?[`Left`,`Right`]:[`Top`,`Bottom`]).reduce((t,n)=>t+(parseFloat(e[`padding${n}`])||0)+(parseFloat(e[`border${n}Width`])||0),r):n}export{l as TabsIndicator,c as useTabsIndicator};
2
+ //# sourceMappingURL=indicator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indicator.js","names":[],"sources":["../../src/tabs/indicator.ts"],"sourcesContent":["import {\n createMixin,\n type FigNode,\n type MixinContext,\n type MixinDescriptor,\n useBeforePaint,\n useMemo,\n} from \"@bgub/fig\";\nimport { bindPart } from \"../internal/parts.ts\";\nimport { createPartSlot } from \"../internal/registration.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\n\nexport interface TabsIndicatorParts {\n indicator(): MixinDescriptor;\n /** Composes with the matching `tabs.list()` descriptor. */\n list(): MixinDescriptor;\n}\n\nexport interface TabsIndicatorProps {\n children: (indicator: TabsIndicatorParts) => FigNode;\n}\n\ntype TabsIndicatorRegistry = ReturnType<typeof createTabsIndicatorRegistry>;\n\nconst indicatorListMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, registry: TabsIndicatorRegistry) => ({\n bind: bindPart(context, registry, registry.bindList),\n }),\n);\n\nconst indicatorMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, registry: TabsIndicatorRegistry) => ({\n bind: bindPart(context, registry, registry.bindIndicator),\n hidden: true,\n role: \"presentation\",\n }),\n);\n\n/**\n * Optional visual behavior for a tabs widget. The core tabs entry owns only\n * selection and accessibility; this hook measures the active tab and publishes\n * its box as `--active-tab-*` CSS properties.\n */\nexport function useTabsIndicator(): TabsIndicatorParts {\n const requestReconcile = useRegistrationReconcile();\n const registry = useMemo(\n () => createTabsIndicatorRegistry(requestReconcile),\n [],\n );\n\n useBeforePaint(() => {\n registry.sync();\n });\n\n return {\n indicator: () => indicatorMixin(registry),\n list: () => indicatorListMixin(registry),\n };\n}\n\n/** {@link useTabsIndicator} as a render-callback component. */\nexport function TabsIndicator(props: TabsIndicatorProps): FigNode {\n return props.children(useTabsIndicator());\n}\n\nfunction createTabsIndicatorRegistry(registrationChanged: () => void) {\n const indicator = createPartSlot(registrationChanged);\n const list = createPartSlot(registrationChanged);\n let observer: ResizeObserver | null = null;\n let observed: HTMLElement[] = [];\n\n function bindIndicator(node: HTMLElement, signal: AbortSignal): void {\n if (!indicator.bind(node, signal)) return;\n signal.addEventListener(\n \"abort\",\n () => {\n if (indicator.node() !== null) return;\n observer?.disconnect();\n observer = null;\n },\n { once: true },\n );\n }\n\n function sync(): void {\n const listNode = list.node();\n const indicatorNode = indicator.node();\n if (indicatorNode === null) {\n observer?.disconnect();\n observer = null;\n return;\n }\n updatePosition();\n\n if (typeof ResizeObserver === \"undefined\") return;\n // Resize delivery must only measure; re-observing would schedule another\n // initial notification even when none of the observed sizes changed.\n if (observer === null) {\n observer = new ResizeObserver(updatePosition);\n observed = [];\n }\n const tabs = ownedTabs(listNode);\n const targets = listNode === null ? tabs : [listNode, ...tabs];\n if (\n targets.length !== observed.length ||\n targets.some((target, index) => target !== observed[index])\n ) {\n observer.disconnect();\n for (const target of targets) observer.observe(target);\n observed = targets;\n }\n }\n\n function updatePosition(): void {\n const indicatorNode = indicator.node();\n if (indicatorNode === null) return;\n const listNode = list.node();\n const active = ownedTabs(listNode).find(\n (tab) => tab.getAttribute(\"aria-selected\") === \"true\",\n );\n positionTabsIndicator(indicatorNode, listNode, active ?? null);\n }\n\n return {\n bindIndicator,\n bindList: list.bind,\n sync,\n };\n}\n\nfunction ownedTabs(list: HTMLElement | null): HTMLElement[] {\n if (list === null) return [];\n return [...list.querySelectorAll<HTMLElement>('[role=\"tab\"]')].filter(\n (tab) => tab.closest('[role=\"tablist\"]') === list,\n );\n}\n\nconst edges = [\"bottom\", \"height\", \"left\", \"right\", \"top\", \"width\"] as const;\n\n/** Publishes the active tab's box as `--active-tab-*` custom properties. */\nfunction positionTabsIndicator(\n indicator: HTMLElement,\n list: HTMLElement | null,\n tab: HTMLElement | null,\n): void {\n const style = indicator.style;\n if (list === null || tab === null) {\n indicator.hidden = true;\n for (const edge of edges) style.removeProperty(`--active-tab-${edge}`);\n return;\n }\n\n const tabRect = tab.getBoundingClientRect();\n const listRect = list.getBoundingClientRect();\n const tabStyle = getComputedStyle(tab);\n const listStyle = getComputedStyle(list);\n const width = borderBoxSize(tabStyle, \"width\", tabRect.width);\n const height = borderBoxSize(tabStyle, \"height\", tabRect.height);\n // Layout dimensions include scrollbar gutters; computed content-box sizes\n // do not. Dividing by the latter mistakes a scrollbar for extra scaling.\n const listWidth =\n list.offsetWidth || borderBoxSize(listStyle, \"width\", listRect.width);\n const listHeight =\n list.offsetHeight || borderBoxSize(listStyle, \"height\", listRect.height);\n const scaleX = listWidth > 0 ? listRect.width / listWidth : 1;\n const scaleY = listHeight > 0 ? listRect.height / listHeight : 1;\n const left = scaleX\n ? (tabRect.left - listRect.left) / scaleX +\n list.scrollLeft -\n list.clientLeft\n : tab.offsetLeft;\n const top = scaleY\n ? (tabRect.top - listRect.top) / scaleY + list.scrollTop - list.clientTop\n : tab.offsetTop;\n const box = {\n bottom: list.scrollHeight - top - height,\n height,\n left,\n right: list.scrollWidth - left - width,\n top,\n width,\n };\n\n for (const edge of edges) {\n style.setProperty(`--active-tab-${edge}`, `${box[edge]}px`);\n }\n indicator.hidden = !(width > 0 && height > 0);\n}\n\nfunction borderBoxSize(\n style: CSSStyleDeclaration,\n dimension: \"width\" | \"height\",\n fallback: number,\n): number {\n const size = parseFloat(style[dimension]);\n if (!Number.isFinite(size)) return fallback;\n if (style.boxSizing === \"border-box\") return size;\n const edges =\n dimension === \"width\"\n ? ([\"Left\", \"Right\"] as const)\n : ([\"Top\", \"Bottom\"] as const);\n return edges.reduce(\n (total, edge) =>\n total +\n (parseFloat(style[`padding${edge}`]) || 0) +\n (parseFloat(style[`border${edge}Width`]) || 0),\n size,\n );\n}\n"],"mappings":"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"}
package/dist/tabs.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { t as ChangeDetails } from "./changes--uvVfvy8.js";
2
+ import { t as Orientation } from "./composite-CG4JghHi.js";
3
+ import { FigNode, MixinDescriptor } from "@bgub/fig";
4
+ //#region src/tabs/registry.d.ts
5
+ type TabsOrientation = Orientation;
6
+ //#endregion
7
+ //#region src/tabs/selection.d.ts
8
+ type TabsValueChangeDetails = ChangeDetails;
9
+ type TabsValueChangeHandler<Value = unknown> = (value: Value | null, details: TabsValueChangeDetails, signal: AbortSignal) => void;
10
+ //#endregion
11
+ //#region src/tabs/tabs.d.ts
12
+ interface TabsListOptions {
13
+ /** Selects a tab as soon as it takes focus. */
14
+ activateOnFocus?: boolean;
15
+ /** Wraps arrow-key movement at both ends. Defaults to `true`. */
16
+ loopFocus?: boolean;
17
+ }
18
+ interface TabsTabOptions {
19
+ disabled?: boolean;
20
+ }
21
+ interface TabsParts<Value = unknown> {
22
+ readonly value: Value | null;
23
+ list(options?: TabsListOptions): MixinDescriptor;
24
+ /** Marks a panel the caller keeps mounted; inactive panels hide. */
25
+ panel(value: Value): MixinDescriptor;
26
+ tab(value: Value, options?: TabsTabOptions): MixinDescriptor;
27
+ }
28
+ interface TabsOptions<Value = unknown> {
29
+ defaultValue?: Value | null;
30
+ onValueChange?: TabsValueChangeHandler<Value>;
31
+ orientation?: TabsOrientation;
32
+ value?: Value | null;
33
+ }
34
+ interface TabsProps<Value = unknown> extends TabsOptions<Value> {
35
+ children: (tabs: TabsParts<Value>) => FigNode;
36
+ }
37
+ /**
38
+ * Coordinates one tabs widget: selection, focus, and the generated
39
+ * relationships between parts. Call it in the component that renders the
40
+ * widget; every element stays the caller's.
41
+ */
42
+ declare function useTabs<Value = unknown>(options?: TabsOptions<Value>): TabsParts<Value>;
43
+ /**
44
+ * {@link useTabs} as a component, for a widget that is not already one of its
45
+ * own. Selection re-renders this root rather than the caller.
46
+ */
47
+ declare function Tabs<Value = unknown>(props: TabsProps<Value>): FigNode;
48
+ //#endregion
49
+ export { Tabs, TabsListOptions, TabsOptions, type TabsOrientation, TabsParts, TabsProps, TabsTabOptions, type TabsValueChangeDetails, type TabsValueChangeHandler, useTabs };
package/dist/tabs.js ADDED
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./changes-DglRuvQa.js";import{a as t,i as n,n as r,t as i}from"./parts-I0jdefyp.js";import"./diagnostics-CoURcwqb.js";import{i as a,n as o,t as s}from"./composite-CiKLX-Ti.js";import{n as c}from"./controllable-value-CDbRzlId.js";import{t as l}from"./ids-Cvu36mFL.js";import{t as u}from"./relations-uMHtgW03.js";import{createMixin as d,useBeforePaint as f,useMemo as p,useStableEvent as m,useState as h}from"@bgub/fig";import{on as g}from"@bgub/fig-dom";const _=d((e,t,n)=>({...b(t),"aria-orientation":t.orientation,bind:r(e,t.registry,(e,n)=>t.registry.bindContainer(e,n)),mix:[g(`click`,i(t.registry,(e,n)=>t.select(e.value,n,e.node))),g(`keydown`,e=>{let r=t.registry.itemAt(e.target);if(r!==void 0){if(!n.activateOnFocus&&!r.disabled&&(e.key===`Enter`||e.key===` `)){e.preventDefault(),t.select(r.value,e,r.node);return}t.registry.moveFocus(e,{loop:n.loopFocus,orientation:t.orientation})}}),g(`focusin`,e=>{let r=t.registry.itemAt(e.target);r!==void 0&&(t.setHighlighted(r.value),n.activateOnFocus&&!r.disabled&&!t.registry.pointerActive()&&t.select(r.value,e,r.node))}),g(`focusout`,e=>{let n=e.currentTarget,r=e.relatedTarget;n instanceof Element&&(!(r instanceof Node)||!n.contains(r))&&t.resetHighlight()}),g(`pointerdown`,(e,n)=>{t.registry.notePointerDown(e.currentTarget,n)})],role:`tablist`})),v=d((e,t,i)=>{let a=i.disabled||e.props.disabled===!0;return{...b(t),...n(e,{disabled:a,id:i.tabId}),"aria-controls":i.panelId,"aria-selected":i.selected?`true`:`false`,"data-active":i.selected?``:void 0,bind:r(e,t.registry,(e,n)=>t.registry.bindItem(e,n,i.value,a)),role:`tab`,tabindex:i.highlighted?0:-1}}),y=d((e,t,n)=>({...b(t),"aria-labelledby":n.tabId,"data-hidden":n.active?void 0:``,bind:r(e,t.registry,(e,r)=>t.registry.bindPanel(e,r,n.value)),hidden:!n.active||void 0,id:e.props.id??n.panelId,inert:!n.active||void 0,role:`tabpanel`,tabindex:n.active?e.props.tabindex??0:-1}));function b(e){return{"data-orientation":e.orientation}}function x(e){let t=s({container:`[role="tablist"]`,item:`[role="tab"]`,name:`tabs`,registrationChanged:e}),n=u(t,e);function r(){n.sync(),t.containerNode()}return{...t,bindPanel:n.bindPanel,sync:r}}const S=Symbol(`fig-headless.tabs.none`);function C(n){let{controlled:r}=n,i=t(),s=c({value:r?n.value:void 0,defaultValue:n.value,onChange:n.onValueChange,equal:a,reconcile:i}),{value:l}=s,[u,d]=h(()=>n.value===null?S:n.value),g=p(()=>({exempt:r||n.implicitDefault?S:n.value,value:l}),[]),_=!r&&n.implicitDefault,v=p(()=>x(i),[]),y=m(e=>{d(()=>e)}),b=m((t,n,i)=>{s.request(()=>t,e(n,i))&&!r&&y(t)}),C=m(()=>{let e=v.items().filter(o),t=(l===null?void 0:e.find(e=>a(e.value,l)))??e[0];t!==void 0&&y(t.value)});return f(()=>{v.sync();let t=v.items(),n=t.filter(o),i=g.value,c=!a(i,l),d=r?null:w(t,l,_,g);if(d!==null){if(a(d.value,i))return;let t=n.find(e=>a(e.value,d.value))??n[0];g.value=d.value,t!==void 0&&y(t.value),s.restore(d.value,e(null));return}g.value=l;let f=T(n,l,u,c&&!v.containsFocus());a(f,u)||y(f)}),{highlightedValue:u,registry:v,resetHighlight:C,select:b,setHighlighted:y,value:l}}function w(e,t,n,r){if(e.length===0)return null;let i=t===null?void 0:e.find(e=>a(e.value,t)),s=i!==void 0&&(i.disabled||!o(i)),c=i===void 0&&t!==null,l=t===null&&n;if(a(t,r.exempt)){if(s)return null;r.exempt=S}if(!s&&!c&&!l)return null;let u=e.find(e=>!e.disabled&&o(e)),d=u===void 0?null:u.value;return a(t,d)?null:{value:d}}function T(e,t,n,r){let i=e.find(e=>a(e.value,n)),o=t===null?void 0:e.find(e=>a(e.value,t));if(i===void 0){let t=o??e[0];return t===void 0?n:t.value}return r&&o!==void 0&&!o.disabled?o.value:n}function E(e={}){let t=e,{orientation:n=`horizontal`}=t,r=t.value,i=r!==void 0,o=C({controlled:i,implicitDefault:!i&&t.defaultValue===void 0,onValueChange:t.onValueChange,value:r===void 0?t.defaultValue??null:r}),{value:s}=o,c=l(),u={orientation:n,registry:o.registry,resetHighlight:o.resetHighlight,select:o.select,setHighlighted:o.setHighlighted};function d(e){return s!==null&&a(e,s)}function f(e){return y(u,{active:d(e),panelId:c(e,`panel`),tabId:c(e,`tab`),value:e})}return{value:s,list:(e={})=>_(u,{activateOnFocus:e.activateOnFocus===!0,loopFocus:e.loopFocus!==!1}),panel:f,tab:(e,t={})=>v(u,{disabled:t.disabled===!0,highlighted:a(o.highlightedValue,e),panelId:c(e,`panel`),selected:d(e),tabId:c(e,`tab`),value:e})}}function D(e){return e.children(E(e))}export{D as Tabs,E as useTabs};
2
+ //# sourceMappingURL=tabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tabs.js","names":[],"sources":["../src/tabs/parts.ts","../src/tabs/registry.ts","../src/tabs/selection.ts","../src/tabs/tabs.tsx"],"sourcesContent":["import { createMixin, type MixinContext } from \"@bgub/fig\";\nimport { on } from \"@bgub/fig-dom\";\nimport { activateOnClick, bindPart, triggerProps } from \"../internal/parts.ts\";\nimport type { TabsOrientation, TabsRegistry } from \"./registry.ts\";\n\n/** Widget-level state every part reads. Built once per root render. */\nexport interface TabsPartState {\n readonly orientation: TabsOrientation;\n readonly registry: TabsRegistry;\n readonly resetHighlight: () => void;\n readonly select: (value: unknown, event: Event, trigger: Element) => void;\n readonly setHighlighted: (value: unknown) => void;\n}\n\ninterface TabsListOwnState {\n readonly activateOnFocus: boolean;\n readonly loopFocus: boolean;\n}\n\ninterface TabsTabOwnState {\n readonly disabled: boolean;\n readonly highlighted: boolean;\n readonly panelId: string;\n readonly selected: boolean;\n readonly tabId: string;\n readonly value: unknown;\n}\n\ninterface TabsPanelOwnState {\n readonly active: boolean;\n readonly panelId: string;\n readonly tabId: string;\n readonly value: unknown;\n}\n\n// The list owns every tab interaction: tabs are plain hosts carrying\n// attributes, so listeners stay O(1) in the number of tabs and each tab's own\n// handlers still run first, in the target phase.\nexport const listMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: TabsPartState, own: TabsListOwnState) => ({\n ...partProps(state),\n \"aria-orientation\": state.orientation,\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindContainer(node, signal),\n ),\n mix: [\n on(\n \"click\",\n activateOnClick(state.registry, (tab, event) =>\n state.select(tab.value, event, tab.node),\n ),\n ),\n on(\"keydown\", (event) => {\n const tab = state.registry.itemAt(event.target);\n if (tab === undefined) return;\n // Automatic activation already selected on focus, and native buttons\n // turn Enter and Space into clicks.\n if (\n !own.activateOnFocus &&\n !tab.disabled &&\n (event.key === \"Enter\" || event.key === \" \")\n ) {\n event.preventDefault();\n state.select(tab.value, event, tab.node);\n return;\n }\n state.registry.moveFocus(event, {\n loop: own.loopFocus,\n orientation: state.orientation,\n });\n }),\n on(\"focusin\", (event) => {\n const tab = state.registry.itemAt(event.target);\n if (tab === undefined) return;\n state.setHighlighted(tab.value);\n if (\n own.activateOnFocus &&\n !tab.disabled &&\n !state.registry.pointerActive()\n ) {\n state.select(tab.value, event, tab.node);\n }\n }),\n on(\"focusout\", (event) => {\n const list = event.currentTarget;\n const next = event.relatedTarget;\n if (\n list instanceof Element &&\n (!(next instanceof Node) || !list.contains(next))\n ) {\n state.resetHighlight();\n }\n }),\n // Click owns primary-pointer activation, so focus must not race it.\n on(\"pointerdown\", (event, signal) => {\n state.registry.notePointerDown(event.currentTarget, signal);\n }),\n ],\n role: \"tablist\",\n }),\n);\n\nexport const tabMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: TabsPartState, own: TabsTabOwnState) => {\n const disabled = own.disabled || context.props.disabled === true;\n return {\n ...partProps(state),\n ...triggerProps(context, { disabled, id: own.tabId }),\n \"aria-controls\": own.panelId,\n \"aria-selected\": own.selected ? \"true\" : \"false\",\n \"data-active\": own.selected ? \"\" : undefined,\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindItem(node, signal, own.value, disabled),\n ),\n role: \"tab\",\n tabindex: own.highlighted ? 0 : -1,\n };\n },\n);\n\nexport const panelMixin = /* @__PURE__ */ createMixin(\n (context: MixinContext, state: TabsPartState, own: TabsPanelOwnState) => ({\n ...partProps(state),\n \"aria-labelledby\": own.tabId,\n \"data-hidden\": own.active ? undefined : \"\",\n bind: bindPart(context, state.registry, (node, signal) =>\n state.registry.bindPanel(node, signal, own.value),\n ),\n hidden: own.active ? undefined : true,\n id: context.props.id ?? own.panelId,\n inert: own.active ? undefined : true,\n role: \"tabpanel\",\n tabindex: own.active ? (context.props.tabindex ?? 0) : -1,\n }),\n);\n\nfunction partProps(state: TabsPartState) {\n return {\n \"data-orientation\": state.orientation,\n };\n}\n","import {\n type CompositeItem,\n createComposite,\n type Orientation,\n sameValue,\n} from \"../internal/composite.ts\";\nimport { createRelations } from \"../internal/relations.ts\";\nimport { assertAccessibleName } from \"../internal/diagnostics.ts\";\n\nexport type TabsOrientation = Orientation;\n\nexport type TabsRegistry = ReturnType<typeof createTabsRegistry>;\n\n/**\n * The shared composite plus what only tabs need: panel relationships.\n */\nexport function createTabsRegistry(registrationChanged: () => void) {\n const composite = createComposite({\n container: '[role=\"tablist\"]',\n item: '[role=\"tab\"]',\n name: \"tabs\",\n registrationChanged,\n });\n\n const relations = createRelations(composite, registrationChanged);\n\n /** Synchronizes panel relationships against the committed DOM. */\n function sync(): void {\n relations.sync();\n const container = composite.containerNode();\n if (container !== null) assertAccessibleName(container, \"tab list\");\n }\n\n return {\n ...composite,\n bindPanel: relations.bindPanel,\n sync,\n };\n}\n\nexport type { CompositeItem as TabsTabRegistration };\nexport { sameValue };\n","import { useBeforePaint, useMemo, useStableEvent, useState } from \"@bgub/fig\";\nimport {\n type ChangeDetails,\n createChangeDetails,\n} from \"../internal/changes.ts\";\nimport {\n type CompositeItem,\n isNativeEnabled,\n sameValue,\n} from \"../internal/composite.ts\";\nimport { useControllableValue } from \"../internal/controllable-value.ts\";\nimport { useRegistrationReconcile } from \"../internal/reconcile.ts\";\nimport { createTabsRegistry } from \"./registry.ts\";\n\nexport type TabsValueChangeDetails = ChangeDetails;\n\nexport type TabsValueChangeHandler<Value = unknown> = (\n value: Value | null,\n details: TabsValueChangeDetails,\n signal: AbortSignal,\n) => void;\n\nexport interface TabsSelectionOptions<Value> {\n readonly controlled: boolean;\n /**\n * Neither `value` nor `defaultValue`, so the root selects the first enabled\n * tab itself. An explicit `defaultValue` of `null` means the opposite: the\n * application asked for no selection.\n */\n readonly implicitDefault: boolean;\n readonly onValueChange: TabsValueChangeHandler<Value> | undefined;\n /** The controlled value, or the initial value of an uncontrolled root. */\n readonly value: Value | null;\n}\n\ninterface SelectionTracker {\n /** An explicitly requested default stays selected even while disabled. */\n exempt: unknown;\n value: unknown;\n}\n\nconst none = Symbol(\"fig-headless.tabs.none\");\n\n/**\n * Owns tab selection: user activation, the roving tab stop, and the automatic\n * repairs an uncontrolled root makes when its tabs change.\n */\nexport function useTabsSelection<Value>(options: TabsSelectionOptions<Value>) {\n const { controlled } = options;\n const registrationChanged = useRegistrationReconcile();\n const selection = useControllableValue<Value | null>({\n value: controlled ? options.value : undefined,\n defaultValue: options.value,\n onChange: options.onValueChange,\n equal: sameValue,\n reconcile: registrationChanged,\n });\n const { value } = selection;\n const [highlighted, setHighlightedState] = useState<unknown>(() =>\n options.value === null ? none : options.value,\n );\n const tracker = useMemo<SelectionTracker>(\n () => ({\n exempt: controlled || options.implicitDefault ? none : options.value,\n value,\n }),\n [],\n );\n const autoSelect = !controlled && options.implicitDefault;\n const registry = useMemo(() => createTabsRegistry(registrationChanged), []);\n\n const setHighlighted = useStableEvent((highlighted: unknown) => {\n setHighlightedState(() => highlighted);\n });\n\n const select = useStableEvent(\n (next: unknown, event: Event, trigger: Element) => {\n if (\n selection.request(\n () => next as Value | null,\n createChangeDetails(event, trigger),\n ) &&\n !controlled\n ) {\n setHighlighted(next);\n }\n },\n );\n\n const resetHighlight = useStableEvent(() => {\n const tabs = registry.items().filter(isNativeEnabled);\n const selected =\n value === null\n ? undefined\n : tabs.find((tab) => sameValue(tab.value, value));\n const next = selected ?? tabs[0];\n if (next !== undefined) setHighlighted(next.value);\n });\n\n useBeforePaint(() => {\n registry.sync();\n const tabs = registry.items();\n const focusable = tabs.filter(isNativeEnabled);\n const previous = tracker.value;\n const changed = !sameValue(previous, value);\n\n const repair = controlled\n ? null\n : planRepair(tabs, value, autoSelect, tracker);\n if (repair !== null) {\n // A repair committed in an earlier pass may not have rendered yet.\n if (sameValue(repair.value, previous)) return;\n const target =\n focusable.find((tab) => sameValue(tab.value, repair.value)) ??\n focusable[0];\n tracker.value = repair.value;\n if (target !== undefined) setHighlighted(target.value);\n selection.restore(\n repair.value as Value | null,\n createChangeDetails(null),\n );\n return;\n }\n tracker.value = value;\n\n const next = nextHighlight(\n focusable,\n value,\n highlighted,\n changed && !registry.containsFocus(),\n );\n if (!sameValue(next, highlighted)) setHighlighted(next);\n });\n\n return {\n highlightedValue: highlighted,\n registry,\n resetHighlight,\n select,\n setHighlighted,\n value,\n };\n}\n\n/**\n * Decides how an uncontrolled root reacts to the tabs it actually has. An\n * automatic change is reported through `onValueChange` with a `null` event,\n * which is what separates it from a user activation.\n */\nfunction planRepair(\n tabs: readonly CompositeItem[],\n value: unknown,\n autoSelect: boolean,\n tracker: SelectionTracker,\n): { readonly value: unknown } | null {\n // Every tab is unregistered: the subtree unmounted or an Activity hid it.\n // Either way the selection is worth keeping for when tabs return.\n if (tabs.length === 0) return null;\n\n const selected =\n value === null\n ? undefined\n : tabs.find((tab) => sameValue(tab.value, value));\n const disabled =\n selected !== undefined && (selected.disabled || !isNativeEnabled(selected));\n const missing = selected === undefined && value !== null;\n // A root that was never told what to select picks the first enabled tab.\n const unselected = value === null && autoSelect;\n\n if (sameValue(value, tracker.exempt)) {\n if (disabled) return null;\n tracker.exempt = none;\n }\n if (!disabled && !missing && !unselected) return null;\n\n const enabled = tabs.find((tab) => !tab.disabled && isNativeEnabled(tab));\n // `undefined` is a usable tab value, so never collapse it with `??`.\n const fallback = enabled === undefined ? null : enabled.value;\n return sameValue(value, fallback) ? null : { value: fallback };\n}\n\n/**\n * Keeps the roving tab stop on a mounted tab. Selection may claim it, but\n * never while the user is navigating inside the list.\n */\nfunction nextHighlight(\n tabs: readonly CompositeItem[],\n value: unknown,\n highlighted: unknown,\n selectionMayClaim: boolean,\n): unknown {\n const current = tabs.find((tab) => sameValue(tab.value, highlighted));\n const selected =\n value === null\n ? undefined\n : tabs.find((tab) => sameValue(tab.value, value));\n if (current === undefined) {\n const fallback = selected ?? tabs[0];\n return fallback === undefined ? highlighted : fallback.value;\n }\n if (selectionMayClaim && selected !== undefined && !selected.disabled) {\n return selected.value;\n }\n return highlighted;\n}\n","import type { FigNode, MixinDescriptor } from \"@bgub/fig\";\nimport { listMixin, panelMixin, tabMixin } from \"./parts.ts\";\nimport { sameValue } from \"../internal/composite.ts\";\nimport { usePartIds } from \"../internal/ids.ts\";\nimport type { TabsOrientation } from \"./registry.ts\";\nimport { type TabsValueChangeHandler, useTabsSelection } from \"./selection.ts\";\n\nexport type { TabsOrientation };\nexport type {\n TabsValueChangeDetails,\n TabsValueChangeHandler,\n} from \"./selection.ts\";\n\nexport interface TabsListOptions {\n /** Selects a tab as soon as it takes focus. */\n activateOnFocus?: boolean;\n /** Wraps arrow-key movement at both ends. Defaults to `true`. */\n loopFocus?: boolean;\n}\n\nexport interface TabsTabOptions {\n disabled?: boolean;\n}\n\nexport interface TabsParts<Value = unknown> {\n readonly value: Value | null;\n list(options?: TabsListOptions): MixinDescriptor;\n /** Marks a panel the caller keeps mounted; inactive panels hide. */\n panel(value: Value): MixinDescriptor;\n tab(value: Value, options?: TabsTabOptions): MixinDescriptor;\n}\n\nexport interface TabsOptions<Value = unknown> {\n defaultValue?: Value | null;\n onValueChange?: TabsValueChangeHandler<Value>;\n orientation?: TabsOrientation;\n value?: Value | null;\n}\n\nexport interface TabsProps<Value = unknown> extends TabsOptions<Value> {\n children: (tabs: TabsParts<Value>) => FigNode;\n}\n\n/**\n * Coordinates one tabs widget: selection, focus, and the generated\n * relationships between parts. Call it in the component that renders the\n * widget; every element stays the caller's.\n */\nexport function useTabs<Value = unknown>(\n options: TabsOptions<Value> = {},\n): TabsParts<Value> {\n const props = options;\n const { orientation = \"horizontal\" } = props;\n const controlledValue = props.value;\n const controlled = controlledValue !== undefined;\n const implicitDefault = !controlled && props.defaultValue === undefined;\n const selection = useTabsSelection<Value>({\n controlled,\n implicitDefault,\n onValueChange: props.onValueChange,\n value:\n controlledValue === undefined\n ? (props.defaultValue ?? null)\n : controlledValue,\n });\n const { value } = selection;\n const idFor = usePartIds();\n const state = {\n orientation,\n registry: selection.registry,\n resetHighlight: selection.resetHighlight,\n select: selection.select,\n setHighlighted: selection.setHighlighted,\n };\n function isSelected(partValue: unknown): boolean {\n return value !== null && sameValue(partValue, value);\n }\n\n function panel(panelValue: Value): MixinDescriptor {\n return panelMixin(state, {\n active: isSelected(panelValue),\n panelId: idFor(panelValue, \"panel\"),\n tabId: idFor(panelValue, \"tab\"),\n value: panelValue,\n });\n }\n\n return {\n value,\n list: (options = {}) =>\n listMixin(state, {\n activateOnFocus: options.activateOnFocus === true,\n loopFocus: options.loopFocus !== false,\n }),\n panel,\n tab: (tabValue, options = {}) =>\n tabMixin(state, {\n disabled: options.disabled === true,\n highlighted: sameValue(selection.highlightedValue, tabValue),\n panelId: idFor(tabValue, \"panel\"),\n selected: isSelected(tabValue),\n tabId: idFor(tabValue, \"tab\"),\n value: tabValue,\n }),\n };\n}\n\n/**\n * {@link useTabs} as a component, for a widget that is not already one of its\n * own. Selection re-renders this root rather than the caller.\n */\nexport function Tabs<Value = unknown>(props: TabsProps<Value>): FigNode {\n return props.children(useTabs(props));\n}\n"],"mappings":"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"}