Skip to content

chore(release): version packages#5

Merged
ivanbanov merged 1 commit into
mainfrom
changeset-release/main
Jul 19, 2026
Merged

chore(release): version packages#5
ivanbanov merged 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@dunky.dev/dialog@0.1.0

Minor Changes

  • #4 3d6981c Thanks @ivanbanov! - Add the Dialog primitive — a modal dialog following the WAI-ARIA APG pattern,
    shipped as an agnostic core (@dunky.dev/dialog) plus a React binding
    (@dunky.dev/react-dialog).

    import { Dialog } from '@dunky.dev/react-dialog'
    
    function App() {
      return (
        <Dialog onOpenChange={console.log}>
          <Dialog.Trigger>Delete...</Dialog.Trigger>
          <Dialog.Portal>
            <Dialog.Backdrop />
            <Dialog.Viewport>
              <Dialog.Content>
                <Dialog.Title>Delete file?</Dialog.Title>
                <Dialog.Description>This cannot be undone.</Dialog.Description>
                <Dialog.Close>Cancel</Dialog.Close>
              </Dialog.Content>
            </Dialog.Viewport>
          </Dialog.Portal>
        </Dialog>
      )
    }

Patch Changes

  • #25 a009501 Thanks @ivanbanov! - The dialog's Close part is now always the focus cycle's last stop, wherever
    it renders — a visually-first close button no longer interrupts the
    content's tab order.

    Mechanism: trapFocus gains a last option resolving the cycle's final
    stop, and now steps focus through the cycle itself on every Tab instead of
    only guarding the edges — a logical order can diverge from DOM order, so
    native tabbing can't be trusted mid-cycle. The dialog's core stays
    substrate-agnostic: Close joins the derived part ids (ids.close), and each
    substrate's containment resolves the element by that id.

  • #17 f339cd5 Thanks @ivanbanov! - Make controlled open truly controlled. A dialog with the open prop set
    never opens or closes on its own — it follows the prop alone. onOpenChange
    now means exactly what it says: it fires on every actual open ⇄ close change,
    whatever drove it (including a prop flip), and never for a dismissal that
    changed nothing. Dismissal decisions happen at their source: preventDefault()
    in onEscapeKeyDown / onInteractOutside, and your own handlers on
    Dialog.Trigger / Dialog.Close.

    const [open, setOpen] = useState(true)
    
    <Dialog
      open={open}
      onOpenChange={setOpen} // fires only when open actually changed
      onEscapeKeyDown={(e) => (canClose ? setOpen(false) : e.preventDefault())}
    >

    Controlled-ness is live, not fixed at mount: set open back to undefined
    and the dialog takes over uncontrolled, right where it stands; supply the
    prop again to retake control.

    Previously an internal dismissal closed a controlled dialog immediately and
    left it out of sync with the prop until the next flip.

    @dunky.dev/react-dialog also now declares react-dom as a peer dependency
    (it renders through a portal — strict installs previously couldn't resolve
    it), and an explicit id={undefined} no longer discards the generated
    SSR-safe id.

  • #24 44ca139 Thanks @ivanbanov! - Internal dependencies on sibling workspace packages are now pinned to an
    exact version instead of a caret range.

    Every package here versions independently — nothing is forced to share a
    version number with anything else. A caret range between two packages that
    both sit above a shared dependency lets a consumer's install resolve to two
    different physical copies of it once those packages' required ranges drift
    apart, silently breaking anything identity-sensitive in that shared
    dependency (a singleton, a WeakMap, module-level state). Pinning exact
    collapses that to one resolvable version: a mismatch now fails at publish
    time instead of surfacing as a runtime bug in a consumer's app.

  • Updated dependencies [ee163cb]:

    • @dunky.dev/controllable@0.1.0

@dunky.dev/controllable@0.1.0

Minor Changes

  • #17 ee163cb Thanks @ivanbanov! - New package: controlled/uncontrolled machinery for @dunky.dev/state-machine
    machines. It encodes one contract every consumer-ownable value shares: a
    controlled machine never moves on its own — only the substrate's
    controlled.sync echo of the prop transitions it, so a change callback bound
    to the state fires exactly when the value changes and never for an intent
    that changed nothing. Controlled-ness follows the prop live: an undefined
    echo hands the value back to the machine where it stands. Uncontrolled, the
    same intent event also takes the transition, so both modes share one
    transition table and one set of guards.

    import { controllable, intent, syncControlled } from '@dunky.dev/controllable'
    
    // context
    open: controllable(options.open) // { controlled, intent }
    
    // transitions — bare `intent` infers from a typed guard; unguarded events
    // have nothing to infer from, so pin the generics once (the `setup.as` idiom)
    const intend = intent.as<StateName, Context, MachineEvent>()
    const synced = syncControlled.as<StateName, Context, MachineEvent>()
    
    close: intend('open', { target: 'closed', value: false }),
    escape: intent('open', { guard: canEscape, target: 'closed', value: false }),
    'controlled.sync': synced('open', { value: false, target: 'closed' }), // every echo re-derives controlled-ness
    
    // connect — the consumer callback reflects the actual state
    reaction(m => m.matches('open'), (open, props) => props.onOpenChange?.(open))

    Each declared intent is also recorded in the context slice's intent slot —
    a fresh token per write — ready for machines that expose a request channel.

    Extracted as a shared core util (rather than dialog-private machinery) so
    every dismissible primitive — popover, tooltip, disclosure — composes the
    same contract instead of hand-rolling it.

@dunky.dev/dom-focus-trap@0.1.0

Minor Changes

  • #25 a009501 Thanks @ivanbanov! - The dialog's Close part is now always the focus cycle's last stop, wherever
    it renders — a visually-first close button no longer interrupts the
    content's tab order.

    Mechanism: trapFocus gains a last option resolving the cycle's final
    stop, and now steps focus through the cycle itself on every Tab instead of
    only guarding the edges — a logical order can diverge from DOM order, so
    native tabbing can't be trusted mid-cycle. The dialog's core stays
    substrate-agnostic: Close joins the derived part ids (ids.close), and each
    substrate's containment resolves the element by that id.

  • #4 599ff3e Thanks @ivanbanov! - Add focus-trap — Tab / Shift+Tab containment for a subtree: focus wraps at both
    ends and never tabs out; Tab is a no-op with no focusables. Ships as the
    framework-free @dunky.dev/dom-focus-trap (trapFocus(container, { enabled }))
    and its React binding @dunky.dev/react-use-focus-trap
    (useFocusTrap(ref, { enabled })), which traps while the component is mounted.

    import { useRef } from 'react'
    import { useFocusTrap } from '@dunky.dev/react-use-focus-trap'
    
    function Panel() {
      const ref = useRef<HTMLDivElement>(null)
      useFocusTrap(ref) // Tab cycles inside the panel while it is mounted
    
      return (
        <div ref={ref} tabIndex={-1} role='dialog'>
          <button type='button'>First</button>
          <button type='button'>Last</button>
        </div>
      )
    }
    // Framework-free: returns a release function; `enabled` is re-checked per Tab.
    import { trapFocus } from '@dunky.dev/dom-focus-trap'
    
    const release = trapFocus(panel, { enabled: () => isTopmost(panel) })
    release()

@dunky.dev/dom-scroll-lock@0.1.0

Minor Changes

  • #4 599ff3e Thanks @ivanbanov! - Add scroll-lock — a reference-counted scroll lock for any container (the page
    body by default), so overlapping holders release in any order, compensating
    both vanished scrollbars with logical padding. Ships as the framework-free
    @dunky.dev/dom-scroll-lock (lockScroll(target?)) and its React binding
    @dunky.dev/react-use-scroll-lock (useScrollLock(locked, target?)), which
    locks while the component is mounted; pass a target to scope the lock to a
    container instead of the page.

    import { useScrollLock } from '@dunky.dev/react-use-scroll-lock'
    
    // Rendered only while the overlay is open, e.g. {open && <ModalPanel />}
    function ModalPanel({ panelRef }: { panelRef?: React.RefObject<HTMLElement> }) {
      useScrollLock() // locks the page while mounted
      // useScrollLock(true, panelRef?.current) // ...or scope it to a container
      return <div role='dialog'>...</div>
    }
    // Framework-free: returns a release; the last holder restores the target.
    import { lockScroll } from '@dunky.dev/dom-scroll-lock'
    
    const releaseBody = lockScroll() // the page body
    const releasePanel = lockScroll(panel) // any scroll container
    releaseBody()
    releasePanel()

@dunky.dev/react-dialog@0.1.0

Minor Changes

  • #4 3d6981c Thanks @ivanbanov! - Add the Dialog primitive — a modal dialog following the WAI-ARIA APG pattern,
    shipped as an agnostic core (@dunky.dev/dialog) plus a React binding
    (@dunky.dev/react-dialog).

    import { Dialog } from '@dunky.dev/react-dialog'
    
    function App() {
      return (
        <Dialog onOpenChange={console.log}>
          <Dialog.Trigger>Delete...</Dialog.Trigger>
          <Dialog.Portal>
            <Dialog.Backdrop />
            <Dialog.Viewport>
              <Dialog.Content>
                <Dialog.Title>Delete file?</Dialog.Title>
                <Dialog.Description>This cannot be undone.</Dialog.Description>
                <Dialog.Close>Cancel</Dialog.Close>
              </Dialog.Content>
            </Dialog.Viewport>
          </Dialog.Portal>
        </Dialog>
      )
    }

Patch Changes

  • #25 a009501 Thanks @ivanbanov! - The dialog's Close part is now always the focus cycle's last stop, wherever
    it renders — a visually-first close button no longer interrupts the
    content's tab order.

    Mechanism: trapFocus gains a last option resolving the cycle's final
    stop, and now steps focus through the cycle itself on every Tab instead of
    only guarding the edges — a logical order can diverge from DOM order, so
    native tabbing can't be trusted mid-cycle. The dialog's core stays
    substrate-agnostic: Close joins the derived part ids (ids.close), and each
    substrate's containment resolves the element by that id.

  • #17 f339cd5 Thanks @ivanbanov! - Make controlled open truly controlled. A dialog with the open prop set
    never opens or closes on its own — it follows the prop alone. onOpenChange
    now means exactly what it says: it fires on every actual open ⇄ close change,
    whatever drove it (including a prop flip), and never for a dismissal that
    changed nothing. Dismissal decisions happen at their source: preventDefault()
    in onEscapeKeyDown / onInteractOutside, and your own handlers on
    Dialog.Trigger / Dialog.Close.

    const [open, setOpen] = useState(true)
    
    <Dialog
      open={open}
      onOpenChange={setOpen} // fires only when open actually changed
      onEscapeKeyDown={(e) => (canClose ? setOpen(false) : e.preventDefault())}
    >

    Controlled-ness is live, not fixed at mount: set open back to undefined
    and the dialog takes over uncontrolled, right where it stands; supply the
    prop again to retake control.

    Previously an internal dismissal closed a controlled dialog immediately and
    left it out of sync with the prop until the next flip.

    @dunky.dev/react-dialog also now declares react-dom as a peer dependency
    (it renders through a portal — strict installs previously couldn't resolve
    it), and an explicit id={undefined} no longer discards the generated
    SSR-safe id.

  • #24 44ca139 Thanks @ivanbanov! - Internal dependencies on sibling workspace packages are now pinned to an
    exact version instead of a caret range.

    Every package here versions independently — nothing is forced to share a
    version number with anything else. A caret range between two packages that
    both sit above a shared dependency lets a consumer's install resolve to two
    different physical copies of it once those packages' required ranges drift
    apart, silently breaking anything identity-sensitive in that shared
    dependency (a singleton, a WeakMap, module-level state). Pinning exact
    collapses that to one resolvable version: a mismatch now fails at publish
    time instead of surfacing as a runtime bug in a consumer's app.

  • Updated dependencies [a009501, f339cd5, 3d6981c, 599ff3e, 44ca139, 599ff3e]:

    • @dunky.dev/dialog@0.1.0
    • @dunky.dev/react-use-focus-trap@0.1.0
    • @dunky.dev/react-use-scroll-lock@0.1.0

@dunky.dev/react-use-focus-trap@0.1.0

Minor Changes

  • #4 599ff3e Thanks @ivanbanov! - Add focus-trap — Tab / Shift+Tab containment for a subtree: focus wraps at both
    ends and never tabs out; Tab is a no-op with no focusables. Ships as the
    framework-free @dunky.dev/dom-focus-trap (trapFocus(container, { enabled }))
    and its React binding @dunky.dev/react-use-focus-trap
    (useFocusTrap(ref, { enabled })), which traps while the component is mounted.

    import { useRef } from 'react'
    import { useFocusTrap } from '@dunky.dev/react-use-focus-trap'
    
    function Panel() {
      const ref = useRef<HTMLDivElement>(null)
      useFocusTrap(ref) // Tab cycles inside the panel while it is mounted
    
      return (
        <div ref={ref} tabIndex={-1} role='dialog'>
          <button type='button'>First</button>
          <button type='button'>Last</button>
        </div>
      )
    }
    // Framework-free: returns a release function; `enabled` is re-checked per Tab.
    import { trapFocus } from '@dunky.dev/dom-focus-trap'
    
    const release = trapFocus(panel, { enabled: () => isTopmost(panel) })
    release()

Patch Changes

  • #24 44ca139 Thanks @ivanbanov! - Internal dependencies on sibling workspace packages are now pinned to an
    exact version instead of a caret range.

    Every package here versions independently — nothing is forced to share a
    version number with anything else. A caret range between two packages that
    both sit above a shared dependency lets a consumer's install resolve to two
    different physical copies of it once those packages' required ranges drift
    apart, silently breaking anything identity-sensitive in that shared
    dependency (a singleton, a WeakMap, module-level state). Pinning exact
    collapses that to one resolvable version: a mismatch now fails at publish
    time instead of surfacing as a runtime bug in a consumer's app.

  • Updated dependencies [a009501, 599ff3e]:

    • @dunky.dev/dom-focus-trap@0.1.0

@dunky.dev/react-use-scroll-lock@0.1.0

Minor Changes

  • #4 599ff3e Thanks @ivanbanov! - Add scroll-lock — a reference-counted scroll lock for any container (the page
    body by default), so overlapping holders release in any order, compensating
    both vanished scrollbars with logical padding. Ships as the framework-free
    @dunky.dev/dom-scroll-lock (lockScroll(target?)) and its React binding
    @dunky.dev/react-use-scroll-lock (useScrollLock(locked, target?)), which
    locks while the component is mounted; pass a target to scope the lock to a
    container instead of the page.

    import { useScrollLock } from '@dunky.dev/react-use-scroll-lock'
    
    // Rendered only while the overlay is open, e.g. {open && <ModalPanel />}
    function ModalPanel({ panelRef }: { panelRef?: React.RefObject<HTMLElement> }) {
      useScrollLock() // locks the page while mounted
      // useScrollLock(true, panelRef?.current) // ...or scope it to a container
      return <div role='dialog'>...</div>
    }
    // Framework-free: returns a release; the last holder restores the target.
    import { lockScroll } from '@dunky.dev/dom-scroll-lock'
    
    const releaseBody = lockScroll() // the page body
    const releasePanel = lockScroll(panel) // any scroll container
    releaseBody()
    releasePanel()

Patch Changes

  • #24 44ca139 Thanks @ivanbanov! - Internal dependencies on sibling workspace packages are now pinned to an
    exact version instead of a caret range.

    Every package here versions independently — nothing is forced to share a
    version number with anything else. A caret range between two packages that
    both sit above a shared dependency lets a consumer's install resolve to two
    different physical copies of it once those packages' required ranges drift
    apart, silently breaking anything identity-sensitive in that shared
    dependency (a singleton, a WeakMap, module-level state). Pinning exact
    collapses that to one resolvable version: a mismatch now fails at publish
    time instead of surfacing as a runtime bug in a consumer's app.

  • Updated dependencies [599ff3e]:

    • @dunky.dev/dom-scroll-lock@0.1.0

@github-actions
github-actions Bot force-pushed the changeset-release/main branch 2 times, most recently from 599b89d to 8ec6558 Compare July 19, 2026 00:17
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 4 times, most recently from e0bcf47 to 5e5b245 Compare July 19, 2026 00:55
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 5e5b245 to f0b4c2f Compare July 19, 2026 13:20
@ivanbanov ivanbanov closed this Jul 19, 2026
@ivanbanov ivanbanov reopened this Jul 19, 2026
@ivanbanov
ivanbanov merged commit 846dc2b into main Jul 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant