Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .changeset/close-last-focus-stop.md

This file was deleted.

39 changes: 0 additions & 39 deletions .changeset/controllable-package.md

This file was deleted.

34 changes: 0 additions & 34 deletions .changeset/controlled-dialog-contract.md

This file was deleted.

30 changes: 0 additions & 30 deletions .changeset/dialog.md

This file was deleted.

35 changes: 0 additions & 35 deletions .changeset/focus-trap.md

This file was deleted.

18 changes: 0 additions & 18 deletions .changeset/pin-internal-deps.md

This file was deleted.

33 changes: 0 additions & 33 deletions .changeset/scroll-lock.md

This file was deleted.

89 changes: 89 additions & 0 deletions packages/core/dialog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# @dunky.dev/dialog

## 0.1.0

### Minor Changes

- [#4](https://github.com/dunky-dev/ui/pull/4) [`3d6981c`](https://github.com/dunky-dev/ui/commit/3d6981c187d20d558c8487391be099acb75c7be4) Thanks [@ivanbanov](https://github.com/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`).

```tsx
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](https://github.com/dunky-dev/ui/pull/25) [`a009501`](https://github.com/dunky-dev/ui/commit/a0095016b4d9b88c0808294a2f9dd0c33609ba14) Thanks [@ivanbanov](https://github.com/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](https://github.com/dunky-dev/ui/pull/17) [`f339cd5`](https://github.com/dunky-dev/ui/commit/f339cd53e5bb62742a0065c262bea573a9692bbe) Thanks [@ivanbanov](https://github.com/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`.

```tsx
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](https://github.com/dunky-dev/ui/pull/24) [`44ca139`](https://github.com/dunky-dev/ui/commit/44ca139ee3f2097ccb1be6496d92e21040d6a531) Thanks [@ivanbanov](https://github.com/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`](https://github.com/dunky-dev/ui/commit/ee163cbfcf57c3da3ae7bb162b6b108b1e83294d)]:
- @dunky.dev/controllable@0.1.0
2 changes: 1 addition & 1 deletion packages/core/dialog/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dunky.dev/dialog",
"version": "0.0.0",
"version": "0.1.0",
"description": "Framework-agnostic dialog interaction, modeled as a state machine.",
"license": "MIT",
"repository": {
Expand Down
41 changes: 41 additions & 0 deletions packages/core/utils/controllable/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# @dunky.dev/controllable

## 0.1.0

### Minor Changes

- [#17](https://github.com/dunky-dev/ui/pull/17) [`ee163cb`](https://github.com/dunky-dev/ui/commit/ee163cbfcf57c3da3ae7bb162b6b108b1e83294d) Thanks [@ivanbanov](https://github.com/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.

```ts
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.
2 changes: 1 addition & 1 deletion packages/core/utils/controllable/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dunky.dev/controllable",
"version": "0.0.0",
"version": "0.1.0",
"description": "Controlled/uncontrolled machinery for state machines: a controllable context slice and gated transitions.",
"license": "MIT",
"repository": {
Expand Down
Loading
Loading