├── .eslintignore ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── config.yml └── workflows │ ├── main.yml │ ├── prepare-release.yml │ ├── release-insiders.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .swcrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.cjs ├── jest ├── create-jest-config.cjs ├── custom-matchers.ts └── polyfills.ts ├── package-lock.json ├── package.json ├── packages ├── @headlessui-react │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── build │ │ └── index.cjs │ ├── jest.config.cjs │ ├── jest.setup.js │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── button │ │ │ │ ├── button.test.tsx │ │ │ │ └── button.tsx │ │ │ ├── checkbox │ │ │ │ ├── checkbox.test.tsx │ │ │ │ └── checkbox.tsx │ │ │ ├── close-button │ │ │ │ └── close-button.tsx │ │ │ ├── combobox-button │ │ │ │ └── combobox-button.tsx │ │ │ ├── combobox-input │ │ │ │ └── combobox-input.tsx │ │ │ ├── combobox-label │ │ │ │ └── combobox-label.tsx │ │ │ ├── combobox-option │ │ │ │ └── combobox-option.tsx │ │ │ ├── combobox-options │ │ │ │ └── combobox-options.tsx │ │ │ ├── combobox │ │ │ │ ├── combobox-machine-glue.tsx │ │ │ │ ├── combobox-machine.ts │ │ │ │ ├── combobox.test.tsx │ │ │ │ └── combobox.tsx │ │ │ ├── data-interactive │ │ │ │ ├── data-interactive.test.tsx │ │ │ │ └── data-interactive.tsx │ │ │ ├── description │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── description.test.tsx.snap │ │ │ │ ├── description.test.tsx │ │ │ │ └── description.tsx │ │ │ ├── dialog-description │ │ │ │ └── dialog-description.tsx │ │ │ ├── dialog-panel │ │ │ │ └── dialog-panel.tsx │ │ │ ├── dialog-title │ │ │ │ └── dialog-title.tsx │ │ │ ├── dialog │ │ │ │ ├── dialog.test.tsx │ │ │ │ └── dialog.tsx │ │ │ ├── disclosure-button │ │ │ │ └── disclosure-button.tsx │ │ │ ├── disclosure-panel │ │ │ │ └── disclosure-panel.tsx │ │ │ ├── disclosure │ │ │ │ ├── disclosure.test.tsx │ │ │ │ └── disclosure.tsx │ │ │ ├── field │ │ │ │ ├── field.test.tsx │ │ │ │ └── field.tsx │ │ │ ├── fieldset │ │ │ │ ├── fieldset.test.tsx │ │ │ │ └── fieldset.tsx │ │ │ ├── focus-trap-features │ │ │ │ └── focus-trap-features.tsx │ │ │ ├── focus-trap │ │ │ │ ├── focus-trap.test.tsx │ │ │ │ └── focus-trap.tsx │ │ │ ├── input │ │ │ │ ├── input.test.tsx │ │ │ │ └── input.tsx │ │ │ ├── keyboard.ts │ │ │ ├── label │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── label.test.tsx.snap │ │ │ │ ├── label.test.tsx │ │ │ │ └── label.tsx │ │ │ ├── legend │ │ │ │ └── legend.tsx │ │ │ ├── listbox-button │ │ │ │ └── listbox-button.tsx │ │ │ ├── listbox-label │ │ │ │ └── listbox-label.tsx │ │ │ ├── listbox-option │ │ │ │ └── listbox-option.tsx │ │ │ ├── listbox-options │ │ │ │ └── listbox-options.tsx │ │ │ ├── listbox-selected-option │ │ │ │ └── listbox-selected-option.tsx │ │ │ ├── listbox │ │ │ │ ├── listbox-machine-glue.tsx │ │ │ │ ├── listbox-machine.ts │ │ │ │ ├── listbox.test.tsx │ │ │ │ └── listbox.tsx │ │ │ ├── menu-button │ │ │ │ └── menu-button.tsx │ │ │ ├── menu-heading │ │ │ │ └── menu-heading.tsx │ │ │ ├── menu-item │ │ │ │ └── menu-item.tsx │ │ │ ├── menu-items │ │ │ │ └── menu-items.tsx │ │ │ ├── menu-section │ │ │ │ └── menu-section.tsx │ │ │ ├── menu-separator │ │ │ │ └── menu-separator.tsx │ │ │ ├── menu │ │ │ │ ├── menu-machine-glue.tsx │ │ │ │ ├── menu-machine.ts │ │ │ │ ├── menu.test.tsx │ │ │ │ └── menu.tsx │ │ │ ├── mouse.ts │ │ │ ├── popover-backdrop │ │ │ │ └── popover-backdrop.tsx │ │ │ ├── popover-button │ │ │ │ └── popover-button.tsx │ │ │ ├── popover-group │ │ │ │ └── popover-group.tsx │ │ │ ├── popover-overlay │ │ │ │ └── popover-overlay.tsx │ │ │ ├── popover-panel │ │ │ │ └── popover-panel.tsx │ │ │ ├── popover │ │ │ │ ├── popover-machine-glue.tsx │ │ │ │ ├── popover-machine.ts │ │ │ │ ├── popover.test.tsx │ │ │ │ └── popover.tsx │ │ │ ├── portal │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── portal.test.tsx.snap │ │ │ │ ├── portal.test.tsx │ │ │ │ └── portal.tsx │ │ │ ├── radio-group-description │ │ │ │ └── radio-group-description.tsx │ │ │ ├── radio-group-label │ │ │ │ └── radio-group-label.tsx │ │ │ ├── radio-group-option │ │ │ │ └── radio-group-option.tsx │ │ │ ├── radio-group │ │ │ │ ├── radio-group.test.tsx │ │ │ │ └── radio-group.tsx │ │ │ ├── radio │ │ │ │ └── radio.tsx │ │ │ ├── select │ │ │ │ ├── select.test.tsx │ │ │ │ └── select.tsx │ │ │ ├── switch-description │ │ │ │ └── switch-description.tsx │ │ │ ├── switch-group │ │ │ │ └── switch-group.tsx │ │ │ ├── switch-label │ │ │ │ └── switch-label.tsx │ │ │ ├── switch │ │ │ │ ├── switch.test.tsx │ │ │ │ └── switch.tsx │ │ │ ├── tab-group │ │ │ │ └── tab-group.tsx │ │ │ ├── tab-list │ │ │ │ └── tab-list.tsx │ │ │ ├── tab-panel │ │ │ │ └── tab-panel.tsx │ │ │ ├── tab-panels │ │ │ │ └── tab-panels.tsx │ │ │ ├── tab │ │ │ │ └── tab.tsx │ │ │ ├── tabs │ │ │ │ ├── tabs.ssr.test.tsx │ │ │ │ ├── tabs.test.tsx │ │ │ │ └── tabs.tsx │ │ │ ├── textarea │ │ │ │ ├── textarea.test.tsx │ │ │ │ └── textarea.tsx │ │ │ ├── tooltip │ │ │ │ └── tooltip.tsx │ │ │ ├── transition-child │ │ │ │ └── transition-child.tsx │ │ │ ├── transition │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── transition.test.tsx.snap │ │ │ │ ├── transition.ssr.test.tsx │ │ │ │ ├── transition.test.tsx │ │ │ │ └── transition.tsx │ │ │ └── transitions │ │ │ │ └── transition.tsx │ │ ├── hooks │ │ │ ├── __mocks__ │ │ │ │ └── use-id.ts │ │ │ ├── document-overflow │ │ │ │ ├── adjust-scrollbar-padding.ts │ │ │ │ ├── handle-ios-locking.ts │ │ │ │ ├── overflow-store.ts │ │ │ │ ├── prevent-scroll.ts │ │ │ │ └── use-document-overflow.ts │ │ │ ├── use-active-press.tsx │ │ │ ├── use-by-comparator.ts │ │ │ ├── use-computed.ts │ │ │ ├── use-controllable.ts │ │ │ ├── use-default-value.ts │ │ │ ├── use-did-element-move.ts │ │ │ ├── use-disposables.ts │ │ │ ├── use-document-event.ts │ │ │ ├── use-element-size.ts │ │ │ ├── use-escape.ts │ │ │ ├── use-event-listener.ts │ │ │ ├── use-event.ts │ │ │ ├── use-flags.ts │ │ │ ├── use-id.ts │ │ │ ├── use-inert-others.test.tsx │ │ │ ├── use-inert-others.tsx │ │ │ ├── use-is-initial-render.ts │ │ │ ├── use-is-mounted.ts │ │ │ ├── use-is-top-layer.ts │ │ │ ├── use-is-touch-device.ts │ │ │ ├── use-iso-morphic-effect.ts │ │ │ ├── use-latest-value.ts │ │ │ ├── use-on-disappear.ts │ │ │ ├── use-on-unmount.ts │ │ │ ├── use-outside-click.ts │ │ │ ├── use-owner.ts │ │ │ ├── use-quick-release.ts │ │ │ ├── use-refocusable-input.ts │ │ │ ├── use-resolve-button-type.ts │ │ │ ├── use-resolved-tag.ts │ │ │ ├── use-root-containers.tsx │ │ │ ├── use-scroll-lock.ts │ │ │ ├── use-server-handoff-complete.ts │ │ │ ├── use-store.ts │ │ │ ├── use-sync-refs.ts │ │ │ ├── use-tab-direction.ts │ │ │ ├── use-text-value.ts │ │ │ ├── use-tracked-pointer.ts │ │ │ ├── use-transition.ts │ │ │ ├── use-tree-walker.ts │ │ │ ├── use-watch.ts │ │ │ └── use-window-event.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── internal │ │ │ ├── close-provider.tsx │ │ │ ├── disabled.tsx │ │ │ ├── floating.tsx │ │ │ ├── focus-sentinel.tsx │ │ │ ├── form-fields.tsx │ │ │ ├── frozen.tsx │ │ │ ├── hidden.tsx │ │ │ ├── id.tsx │ │ │ ├── open-closed.tsx │ │ │ └── portal-force-root.tsx │ │ ├── machine.ts │ │ ├── machines │ │ │ └── stack-machine.ts │ │ ├── react-glue.tsx │ │ ├── test-utils │ │ │ ├── accessibility-assertions.ts │ │ │ ├── execute-timeline.ts │ │ │ ├── fake-pointer.ts │ │ │ ├── interactions.test.tsx │ │ │ ├── interactions.ts │ │ │ ├── report-dom-node-changes.ts │ │ │ ├── scenarios.tsx │ │ │ ├── snapshot.ts │ │ │ ├── ssr.tsx │ │ │ └── suppress-console-logs.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── __snapshots__ │ │ │ └── render.test.tsx.snap │ │ │ ├── active-element-history.ts │ │ │ ├── bugs.ts │ │ │ ├── calculate-active-index.ts │ │ │ ├── class-names.ts │ │ │ ├── default-map.ts │ │ │ ├── disposables.ts │ │ │ ├── document-ready.ts │ │ │ ├── dom.ts │ │ │ ├── env.ts │ │ │ ├── focus-management.ts │ │ │ ├── form.test.ts │ │ │ ├── form.ts │ │ │ ├── get-text-value.test.ts │ │ │ ├── get-text-value.ts │ │ │ ├── match.ts │ │ │ ├── micro-task.ts │ │ │ ├── once.ts │ │ │ ├── owner.ts │ │ │ ├── platform.ts │ │ │ ├── render.test.tsx │ │ │ ├── render.ts │ │ │ ├── stable-collection.tsx │ │ │ ├── start-transition.ts │ │ │ └── store.ts │ ├── tsconfig.json │ └── types │ │ └── jest.d.ts ├── @headlessui-tailwindcss │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── build │ │ └── index.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── scripts │ │ └── fix-types.cjs │ ├── src │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.json └── @headlessui-vue │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── build │ └── index.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ ├── components │ │ ├── combobox │ │ │ ├── combobox.test.ts │ │ │ └── combobox.ts │ │ ├── description │ │ │ ├── __snapshots__ │ │ │ │ └── description.test.ts.snap │ │ │ ├── description.test.ts │ │ │ └── description.ts │ │ ├── dialog │ │ │ ├── dialog.test.ts │ │ │ └── dialog.ts │ │ ├── disclosure │ │ │ ├── disclosure.srr.test.ts │ │ │ ├── disclosure.test.ts │ │ │ └── disclosure.ts │ │ ├── focus-trap │ │ │ ├── focus-trap.test.ts │ │ │ └── focus-trap.ts │ │ ├── label │ │ │ ├── __snapshots__ │ │ │ │ └── label.test.ts.snap │ │ │ ├── label.test.ts │ │ │ └── label.ts │ │ ├── listbox │ │ │ ├── listbox.test.tsx │ │ │ └── listbox.ts │ │ ├── menu │ │ │ ├── menu.test.tsx │ │ │ └── menu.ts │ │ ├── popover │ │ │ ├── popover.test.ts │ │ │ └── popover.ts │ │ ├── portal │ │ │ ├── __snapshots__ │ │ │ │ └── portal.test.ts.snap │ │ │ ├── portal.test.ts │ │ │ └── portal.ts │ │ ├── radio-group │ │ │ ├── radio-group.test.ts │ │ │ └── radio-group.ts │ │ ├── switch │ │ │ ├── switch.test.tsx │ │ │ └── switch.ts │ │ ├── tabs │ │ │ ├── tabs.ssr.test.ts │ │ │ ├── tabs.test.ts │ │ │ └── tabs.ts │ │ └── transitions │ │ │ ├── __snapshots__ │ │ │ └── transition.test.ts.snap │ │ │ ├── transition.ssr.test.ts │ │ │ ├── transition.test.ts │ │ │ ├── transition.ts │ │ │ └── utils │ │ │ ├── transition.test.ts │ │ │ └── transition.ts │ ├── hooks │ │ ├── __mocks__ │ │ │ └── use-id.ts │ │ ├── document-overflow │ │ │ ├── adjust-scrollbar-padding.ts │ │ │ ├── handle-ios-locking.ts │ │ │ ├── overflow-store.ts │ │ │ ├── prevent-scroll.ts │ │ │ └── use-document-overflow.ts │ │ ├── use-controllable.ts │ │ ├── use-disposables.ts │ │ ├── use-document-event.ts │ │ ├── use-event-listener.ts │ │ ├── use-frame-debounce.ts │ │ ├── use-id.ts │ │ ├── use-inert.test.ts │ │ ├── use-inert.ts │ │ ├── use-outside-click.ts │ │ ├── use-resolve-button-type.ts │ │ ├── use-root-containers.ts │ │ ├── use-store.ts │ │ ├── use-tab-direction.ts │ │ ├── use-text-value.ts │ │ ├── use-tracked-pointer.ts │ │ ├── use-tree-walker.ts │ │ └── use-window-event.ts │ ├── index.test.ts │ ├── index.ts │ ├── internal │ │ ├── dom-containers.ts │ │ ├── focus-sentinel.ts │ │ ├── hidden.ts │ │ ├── open-closed.ts │ │ ├── portal-force-root.ts │ │ └── stack-context.ts │ ├── keyboard.ts │ ├── mouse.ts │ ├── test-utils │ │ ├── accessibility-assertions.ts │ │ ├── execute-timeline.ts │ │ ├── fake-pointer.ts │ │ ├── html.ts │ │ ├── interactions.test.ts │ │ ├── interactions.ts │ │ ├── report-dom-node-changes.ts │ │ ├── ssr.ts │ │ ├── suppress-console-logs.ts │ │ └── vue-testing-library.ts │ └── utils │ │ ├── active-element-history.ts │ │ ├── calculate-active-index.ts │ │ ├── disposables.ts │ │ ├── document-ready.ts │ │ ├── dom.ts │ │ ├── env.ts │ │ ├── focus-management.ts │ │ ├── form.test.ts │ │ ├── form.ts │ │ ├── get-text-value.test.ts │ │ ├── get-text-value.ts │ │ ├── match.ts │ │ ├── micro-task.ts │ │ ├── once.ts │ │ ├── owner.ts │ │ ├── pipeline.ts │ │ ├── platform.ts │ │ ├── render.test.ts │ │ ├── render.ts │ │ ├── resolve-prop-value.ts │ │ └── store.ts │ ├── tsconfig.json │ └── types │ └── jest.d.ts ├── playgrounds ├── react │ ├── components │ │ └── button.tsx │ ├── data.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── _error.tsx │ │ ├── combinations │ │ │ ├── form.tsx │ │ │ └── tabs-in-dialog.tsx │ │ ├── combobox │ │ │ ├── combobox-countries.tsx │ │ │ ├── combobox-open-on-focus.tsx │ │ │ ├── combobox-virtual-with-empty-states.tsx │ │ │ ├── combobox-virtualized.tsx │ │ │ ├── combobox-with-pure-tailwind.tsx │ │ │ ├── command-palette-with-groups.tsx │ │ │ ├── command-palette.tsx │ │ │ └── multi-select.tsx │ │ ├── dialog │ │ │ ├── dialog-built-in-transition.tsx │ │ │ ├── dialog-focus-issue.tsx │ │ │ ├── dialog-scroll-issue.tsx │ │ │ ├── dialog-with-shadow-children.tsx │ │ │ ├── dialog.tsx │ │ │ ├── scrollable-dialog.tsx │ │ │ ├── scrollable-page-with-dialog.tsx │ │ │ └── sibling-dialogs.tsx │ │ ├── disclosure │ │ │ └── disclosure.tsx │ │ ├── listbox │ │ │ ├── listbox-with-pure-tailwind.tsx │ │ │ ├── multi-select.tsx │ │ │ └── multiple-elements.tsx │ │ ├── menu │ │ │ ├── menu-with-floating-ui.tsx │ │ │ ├── menu-with-framer-motion.tsx │ │ │ ├── menu-with-popper.tsx │ │ │ ├── menu-with-transition-and-popper.tsx │ │ │ ├── menu-with-transition.tsx │ │ │ ├── menu.tsx │ │ │ └── multiple-elements.tsx │ │ ├── popover │ │ │ └── popover.tsx │ │ ├── radio-group │ │ │ └── radio-group.tsx │ │ ├── styles.css │ │ ├── suspense │ │ │ └── portal.tsx │ │ ├── switch │ │ │ └── switch-with-pure-tailwind.tsx │ │ ├── tabs │ │ │ └── tabs-with-pure-tailwind.tsx │ │ └── transitions │ │ │ ├── appear.tsx │ │ │ ├── both-apis.tsx │ │ │ ├── component-examples │ │ │ ├── dropdown.tsx │ │ │ ├── modal.tsx │ │ │ ├── nested │ │ │ │ ├── hidden.tsx │ │ │ │ └── unmount.tsx │ │ │ └── peek-a-boo.tsx │ │ │ ├── full-page-examples │ │ │ ├── full-page-transition.tsx │ │ │ └── layout-with-sidebar.tsx │ │ │ └── react-hot-toast.tsx │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── tsconfig.json │ └── utils │ │ ├── class-names.ts │ │ ├── hooks │ │ └── use-popper.ts │ │ ├── match.ts │ │ └── resolve-all-examples.ts └── vue │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── favicon.ico │ ├── src │ ├── .generated │ │ └── .gitignore │ ├── App.vue │ ├── KeyCaster.vue │ ├── Layout.vue │ ├── components │ │ ├── Button.vue │ │ ├── Home.vue │ │ ├── combinations │ │ │ ├── form.vue │ │ │ └── tabs-in-dialog.vue │ │ ├── combobox │ │ │ ├── _virtual-example.vue │ │ │ ├── combobox-countries.vue │ │ │ ├── combobox-open-on-focus.vue │ │ │ ├── combobox-virtual-with-empty-states.vue │ │ │ ├── combobox-virtualized.vue │ │ │ ├── combobox-with-pure-tailwind.vue │ │ │ ├── command-palette-with-groups.vue │ │ │ ├── command-palette.vue │ │ │ └── multi-select.vue │ │ ├── dialog │ │ │ ├── dialog.vue │ │ │ └── slide-over.vue │ │ ├── disclosure │ │ │ └── disclosure.vue │ │ ├── focus-trap │ │ │ └── focus-trap.vue │ │ ├── listbox │ │ │ ├── listbox.vue │ │ │ ├── multi-select.vue │ │ │ └── multiple-elements.vue │ │ ├── menu │ │ │ ├── menu-with-floating-ui.vue │ │ │ ├── menu-with-popper.vue │ │ │ ├── menu-with-transition-and-popper.vue │ │ │ ├── menu-with-transition.vue │ │ │ ├── menu.vue │ │ │ └── multiple-elements.vue │ │ ├── popover │ │ │ └── popover.vue │ │ ├── portal │ │ │ └── portal.vue │ │ ├── radio-group │ │ │ └── radio-group.vue │ │ ├── switch │ │ │ └── switch.vue │ │ └── tabs │ │ │ ├── simple-tabs.vue │ │ │ └── tabs.vue │ ├── data.ts │ ├── main.ts │ ├── playground-utils │ │ └── hooks │ │ │ └── use-popper.js │ ├── router.ts │ └── styles.css │ ├── tsconfig.json │ ├── vercel.json │ └── vite.config.js └── scripts ├── build.sh ├── lint.sh ├── make-nextjs-happy.js ├── package-path.js ├── release-channel.js ├── release-notes.js ├── resolve-files.js ├── rewrite-imports.js ├── test.sh └── watch.sh /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /packages/**/dist 3 | /node_modules 4 | /packages/**/node_modules -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: If you've already asked for help with a problem and confirmed something is broken with Headless UI itself, create a bug report. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | **What package within Headless UI are you using?** 12 | 13 | For example: @headlessui/react 14 | 15 | **What version of that package are you using?** 16 | 17 | For example: v0.3.1 18 | 19 | **What browser are you using?** 20 | 21 | For example: Chrome, Safari, or N/A 22 | 23 | **Reproduction URL** 24 | 25 | A public GitHub repo that includes a minimal reproduction of the bug. **Please do not link to your actual project**, what we need instead is a _minimal_ reproduction in a fresh project without any unnecessary code. This means it doesn't matter if your real project is private/confidential, since we want a link to a separate, isolated reproduction anyways. Unfortunately we can't provide support without a reproduction, and your issue will be closed with no comment if this is not provided. 26 | 27 | You can use one of the starting projects on CodeSandbox: 28 | 29 | - With React and Tailwind CSS: https://codesandbox.io/s/github/tailwindlabs/reproduction-headlessui-react 30 | - With Vue and Tailwind CSS: https://codesandbox.io/s/github/tailwindlabs/reproduction-headlessui-vue 31 | 32 | **Describe your issue** 33 | 34 | Describe the problem you're seeing, any important steps to reproduce and what behavior you expect instead. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Get Help 4 | url: https://github.com/tailwindlabs/headlessui/discussions/new?category=help 5 | about: If you can't get something to work the way you expect, open a question in our discussion forums. 6 | - name: Feature Request 7 | url: https://github.com/tailwindlabs/headlessui/discussions/new?category=ideas 8 | about: 'Suggest any ideas you have using our discussion forums.' 9 | - name: Documentation Issue 10 | url: https://github.com/tailwindlabs/headlessui/issues/new?title=%5BDOCS%5D:%20 11 | about: 'For documentation issues, suggest changes on our documentation repository.' 12 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - '**' 8 | 9 | env: 10 | CI: true 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | permissions: 18 | contents: write # for softprops/action-gh-release to create GitHub release 19 | 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | matrix: 24 | node-version: [18] 25 | 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - run: git fetch --tags -f 30 | 31 | - name: Resolve version 32 | id: vars 33 | run: | 34 | echo "TAG_NAME=${{ github.ref_name }}" >> $GITHUB_ENV 35 | 36 | - name: Get release notes 37 | run: | 38 | RELEASE_NOTES=$(npm run release-notes $TAG_NAME --silent) 39 | echo "RELEASE_NOTES<> $GITHUB_ENV 40 | echo "$RELEASE_NOTES" >> $GITHUB_ENV 41 | echo "EOF" >> $GITHUB_ENV 42 | 43 | - name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | registry-url: 'https://registry.npmjs.org' 48 | 49 | - name: Release 50 | uses: softprops/action-gh-release@v2 51 | with: 52 | draft: true 53 | tag_name: ${{ env.TAG_NAME }} 54 | body: | 55 | ${{ env.RELEASE_NOTES }} 56 | -------------------------------------------------------------------------------- /.github/workflows/release-insiders.yml: -------------------------------------------------------------------------------- 1 | name: Release Insiders 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 9 | cancel-in-progress: true 10 | 11 | permissions: 12 | contents: read 13 | # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions 14 | id-token: write 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [20] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | registry-url: 'https://registry.npmjs.org' 32 | 33 | - name: Use cached node_modules 34 | id: cache 35 | uses: actions/cache@v4 36 | with: 37 | path: '**/node_modules' 38 | key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('**/package-lock.json') }} 39 | restore-keys: | 40 | nodeModules- 41 | 42 | - name: Install dependencies 43 | run: npm ci 44 | env: 45 | CI: true 46 | 47 | - name: Test 48 | run: | 49 | npm run test || npm run test || npm run test || exit 1 50 | env: 51 | CI: true 52 | 53 | - name: Resolve version 54 | id: vars 55 | run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 56 | 57 | - name: 'Version based on commit: 0.0.0-insiders.${{ steps.vars.outputs.sha_short }}' 58 | run: npm version -w packages 0.0.0-insiders.${{ steps.vars.outputs.sha_short }} --force --no-git-tag-version 59 | 60 | - name: Publish 61 | run: npm publish -w packages --provenance --tag insiders 62 | env: 63 | CI: true 64 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 9 | cancel-in-progress: true 10 | 11 | permissions: 12 | contents: read 13 | # https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions 14 | id-token: write 15 | 16 | env: 17 | CI: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | matrix: 25 | node-version: [18] 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | registry-url: 'https://registry.npmjs.org' 35 | 36 | - name: Use cached node_modules 37 | id: cache 38 | uses: actions/cache@v4 39 | with: 40 | path: '**/node_modules' 41 | key: ${{ runner.os }}-${{ env.NODE_VERSION }}-modules-${{ hashFiles('**/package-lock.json') }} 42 | restore-keys: | 43 | nodeModules- 44 | 45 | - name: Install dependencies 46 | run: npm ci 47 | env: 48 | CI: true 49 | 50 | - name: Test 51 | run: | 52 | npm run test || npm run test || npm run test || exit 1 53 | env: 54 | CI: true 55 | 56 | - name: Calculate environment variables 57 | run: | 58 | echo "TAG_NAME=${{ github.event.tag_name }}" >> $GITHUB_ENV 59 | echo "RELEASE_CHANNEL=$(npm run release-channel $TAG_NAME --silent)" >> $GITHUB_ENV 60 | echo "PACKAGE_PATH=$(npm run package-path $TAG_NAME --silent)" >> $GITHUB_ENV 61 | 62 | - name: Publish 63 | run: npm publish ${{ env.PACKAGE_PATH }} --provenance --tag ${{ env.RELEASE_CHANNEL }} 64 | env: 65 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /packages/**/node_modules 6 | /playgrounds/**/node_modules 7 | 8 | # testing 9 | /coverage 10 | /packages/**/coverage 11 | /playgrounds/**/coverage 12 | 13 | # logs 14 | *.log 15 | /packages/**/*.log 16 | /playgrounds/**/*.log 17 | 18 | # next.js 19 | /.next/ 20 | /playgrounds/**/.next/ 21 | /out/ 22 | /packages/**/out/ 23 | /playgrounds/**/out/ 24 | 25 | # production 26 | /dist 27 | /packages/**/dist 28 | /playgrounds/**/dist 29 | 30 | # misc 31 | .DS_Store 32 | *.pem 33 | .cache 34 | 35 | # debug 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | 40 | # local env files 41 | .env.local 42 | .env.development.local 43 | .env.test.local 44 | .env.production.local 45 | 46 | # vercel 47 | .vercel 48 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | coverage/ 4 | .next/ 5 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "minify": false, 3 | "jsc": { 4 | "parser": { 5 | "syntax": "typescript", 6 | "tsx": true, 7 | "decorators": false, 8 | "dynamicImport": false 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Each package has its own changelog. 4 | 5 | - [@headlessui/react](https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-react/CHANGELOG.md) 6 | - [@headlessui/vue](https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-vue/CHANGELOG.md) 7 | - [@headlessui/tailwindcss](https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-tailwindcss/CHANGELOG.md) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tailwind Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: ['/packages/*/jest.config.cjs'], 3 | } 4 | -------------------------------------------------------------------------------- /jest/create-jest-config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function createJestConfig(root, options) { 2 | let { setupFilesAfterEnv = [], transform = {}, ...rest } = options 3 | return Object.assign( 4 | { 5 | rootDir: root, 6 | setupFilesAfterEnv: [ 7 | '../../jest/custom-matchers.ts', 8 | '../../jest/polyfills.ts', 9 | ...setupFilesAfterEnv, 10 | ], 11 | transform: { 12 | '^.+\\.(t|j)sx?$': '@swc/jest', 13 | ...transform, 14 | }, 15 | globals: { 16 | __DEV__: true, 17 | }, 18 | }, 19 | rest 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /jest/custom-matchers.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect' 2 | 3 | // Assuming requestAnimationFrame is roughly 60 frames per second 4 | let frame = 1000 / 60 5 | let amountOfFrames = 2 6 | 7 | let formatter = new Intl.NumberFormat('en') 8 | 9 | expect.extend({ 10 | toBeWithinRenderFrame(actual, expected) { 11 | let min = expected - frame * amountOfFrames 12 | let max = expected + frame * amountOfFrames 13 | 14 | let pass = actual >= min && actual <= max 15 | 16 | return { 17 | message: pass 18 | ? () => { 19 | return `expected ${actual} not to be within range of a frame ${formatter.format( 20 | min 21 | )} - ${formatter.format(max)}` 22 | } 23 | : () => { 24 | return `expected ${actual} not to be within range of a frame ${formatter.format( 25 | min 26 | )} - ${formatter.format(max)}` 27 | }, 28 | pass, 29 | } 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /jest/polyfills.ts: -------------------------------------------------------------------------------- 1 | import { mockAnimationsApi, mockResizeObserver } from 'jsdom-testing-mocks' 2 | 3 | mockAnimationsApi() // `Element.prototype.getAnimations` and `CSSTransition` polyfill 4 | mockResizeObserver() // `ResizeObserver` polyfill 5 | 6 | // JSDOM Doesn't implement innerText yet: https://github.com/jsdom/jsdom/issues/1245 7 | // So this is a hacky way of implementing it using `textContent`. 8 | // Real implementation doesn't use textContent because: 9 | // > textContent gets the content of all elements, including 13 | 14 | 15 | -------------------------------------------------------------------------------- /playgrounds/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-vue", 3 | "private": true, 4 | "version": "0.0.0", 5 | "directories": { 6 | "example": "examples" 7 | }, 8 | "scripts": { 9 | "prebuild": "npm run build --workspace=@headlessui/vue && npm run build --workspace=@headlessui/tailwindcss", 10 | "predev": "npm run build --workspace=@headlessui/vue && npm run build --workspace=@headlessui/tailwindcss", 11 | "dev:tailwindcss": "npm run watch --workspace=@headlessui/tailwindcss", 12 | "dev:headlessui": "npm run watch --workspace=@headlessui/vue", 13 | "dev:next": "vite serve", 14 | "dev": "npm-run-all -p dev:*", 15 | "build": "NODE_ENV=production vite build", 16 | "lint-types": "echo", 17 | "clean": "rimraf ./dist" 18 | }, 19 | "dependencies": { 20 | "@headlessui/vue": "*", 21 | "@heroicons/vue": "^1.0.6", 22 | "@tailwindcss/forms": "^0.5.2", 23 | "@tailwindcss/postcss": "^4.1.3", 24 | "@tailwindcss/typography": "^0.5.2", 25 | "postcss": "^8.4.14", 26 | "tailwindcss": "^4.1.3", 27 | "vue": "^3.4.27", 28 | "vue-flatpickr-component": "^9.0.5", 29 | "vue-router": "^4.3.2" 30 | }, 31 | "devDependencies": { 32 | "@floating-ui/vue": "^1.0.2", 33 | "@vitejs/plugin-vue": "^5.0.5", 34 | "vite": "^5.2.12" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /playgrounds/vue/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /playgrounds/vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailwindlabs/headlessui/bc76bbd3f153dbff7b9a2e9e3aace29da92c37a0/playgrounds/vue/public/favicon.ico -------------------------------------------------------------------------------- /playgrounds/vue/src/.generated/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /playgrounds/vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | -------------------------------------------------------------------------------- /playgrounds/vue/src/KeyCaster.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 70 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/Button.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 43 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/combinations/tabs-in-dialog.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 32 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/combobox/combobox-virtualized.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 38 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/disclosure/disclosure.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/focus-trap/focus-trap.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/portal/portal.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/switch/switch.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 46 | -------------------------------------------------------------------------------- /playgrounds/vue/src/components/tabs/simple-tabs.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /playgrounds/vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | // @ts-expect-error TODO: Properly handle this 3 | import App from './App.vue' 4 | import router from './router' 5 | 6 | import './styles.css' 7 | 8 | createApp(App).use(router).mount('#app') 9 | -------------------------------------------------------------------------------- /playgrounds/vue/src/playground-utils/hooks/use-popper.js: -------------------------------------------------------------------------------- 1 | import { createPopper } from '@popperjs/core' 2 | import { onMounted, ref, watchEffect } from 'vue' 3 | 4 | export function usePopper(options) { 5 | let reference = ref(null) 6 | let popper = ref(null) 7 | 8 | onMounted(() => { 9 | watchEffect((onInvalidate) => { 10 | if (!popper.value) return 11 | if (!reference.value) return 12 | 13 | let popperEl = popper.value.el || popper.value 14 | let referenceEl = reference.value.el || reference.value 15 | 16 | if (!(referenceEl instanceof HTMLElement)) return 17 | if (!(popperEl instanceof HTMLElement)) return 18 | 19 | let { destroy } = createPopper(referenceEl, popperEl, options) 20 | 21 | onInvalidate(destroy) 22 | }) 23 | }) 24 | 25 | return [reference, popper] 26 | } 27 | -------------------------------------------------------------------------------- /playgrounds/vue/src/styles.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin '@tailwindcss/forms'; 3 | @plugin '@tailwindcss/typography'; 4 | @plugin '@headlessui/tailwindcss'; 5 | 6 | /* 7 | The default border color has changed to `currentcolor` in Tailwind CSS v4, 8 | so we've added these compatibility styles to make sure everything still 9 | looks the same as it did with Tailwind CSS v3. 10 | 11 | If we ever want to remove these styles, we need to add an explicit border 12 | color utility to any element that depends on these defaults. 13 | */ 14 | @layer base { 15 | *, 16 | ::after, 17 | ::before, 18 | ::backdrop, 19 | ::file-selector-button { 20 | border-color: var(--color-gray-200, currentcolor); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /playgrounds/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "types": ["vite/client"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "incremental": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "downlevelIteration": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["**/*.ts", "**/*.tsx", "**/*.vue"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /playgrounds/vue/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /playgrounds/vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | server: { port: 3000 }, 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ROOT_DIR="$(git rev-parse --show-toplevel)/" 5 | TARGET_DIR="$(pwd)" 6 | RELATIVE_TARGET_DIR="${TARGET_DIR/$ROOT_DIR/}" 7 | 8 | # INFO: This script is always run from the root of the repository. If we execute this script from a 9 | # package then the filters (in this case a path to $RELATIVE_TARGET_DIR) will be applied. 10 | 11 | pushd $ROOT_DIR > /dev/null 12 | 13 | prettierArgs=() 14 | 15 | if ! [ -z "$CI" ]; then 16 | prettierArgs+=("--check") 17 | else 18 | prettierArgs+=("--write") 19 | fi 20 | 21 | # Add default arguments 22 | prettierArgs+=('--ignore-unknown') 23 | 24 | # Passthrough arguments and flags 25 | prettierArgs+=($@) 26 | 27 | # Ensure that a path is passed, otherwise default to the current directory 28 | if [ -z "$@" ]; then 29 | prettierArgs+=("$RELATIVE_TARGET_DIR") 30 | fi 31 | 32 | # Execute 33 | npx prettier "${prettierArgs[@]}" 34 | 35 | popd > /dev/null 36 | -------------------------------------------------------------------------------- /scripts/make-nextjs-happy.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import prettier from 'prettier' 4 | import * as HUI from '../packages/@headlessui-react/src/index.ts' 5 | 6 | let customRemaps = { 7 | tab: 'tabs', 8 | radio: 'radio-group', 9 | data: 'data-interactive', 10 | focus: 'focus-trap', 11 | } 12 | 13 | let components = Object.keys(HUI) 14 | 15 | async function run() { 16 | for (let component of components) { 17 | let name = component.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() 18 | let module = name.split('-')[0] 19 | module = customRemaps[module] ?? module 20 | 21 | let filePath = path.resolve( 22 | __dirname, 23 | '..', 24 | 'packages', 25 | '@headlessui-react', 26 | 'src', 27 | 'components', 28 | name, 29 | `${name}.tsx` 30 | ) 31 | 32 | // Main module path already exists 33 | if (!fs.existsSync(filePath)) { 34 | fs.mkdirSync(path.dirname(filePath), { recursive: true }) 35 | fs.writeFileSync(filePath, await template(component, module)) 36 | } 37 | } 38 | } 39 | 40 | async function template(name, module) { 41 | return await prettier.format( 42 | [ 43 | '// Next.js barrel file improvements (GENERATED FILE)', 44 | `export type * from '../${module}/${module}';`, 45 | `export { ${name} } from '../${module}/${module}';`, 46 | ].join('\n'), 47 | { parser: 'typescript' } 48 | ) 49 | } 50 | 51 | run() 52 | -------------------------------------------------------------------------------- /scripts/package-path.js: -------------------------------------------------------------------------------- 1 | // Given a version, figure out what the release notes are so that we can use this to pre-fill the 2 | // relase notes on a GitHub release for the current version. 3 | 4 | let path = require('path') 5 | let { execSync } = require('child_process') 6 | 7 | let tag = process.argv[2] || execSync(`git describe --tags --abbrev=0`).toString().trim() 8 | let pkgPath = path.resolve( 9 | __dirname, 10 | '..', 11 | 'packages', 12 | tag.slice(0, tag.indexOf('@', 1)).replace('/', '-') 13 | ) 14 | 15 | console.log('./' + path.relative(process.cwd(), pkgPath)) 16 | -------------------------------------------------------------------------------- /scripts/release-channel.js: -------------------------------------------------------------------------------- 1 | let path = require('path') 2 | let { execSync } = require('child_process') 3 | 4 | // Given a version, figure out what the release channel is so that we can publish to the correct 5 | // channel on npm. 6 | // 7 | // E.g.: 8 | // 9 | // 1.2.3 -> latest (default) 10 | // 0.0.0-insiders.ffaa88 -> insiders 11 | // 4.1.0-alpha.4 -> alpha 12 | 13 | let tag = process.argv[2] || execSync(`git describe --tags --abbrev=0`).toString().trim() 14 | let pkgPath = path.resolve( 15 | __dirname, 16 | '..', 17 | 'packages', 18 | tag.slice(0, tag.indexOf('@', 1)).replace('/', '-') 19 | ) 20 | 21 | let version = require(path.resolve(pkgPath, 'package.json')).version 22 | 23 | let match = /\d+\.\d+\.\d+-(.*)\.\d+/g.exec(version) 24 | if (match) { 25 | // We want to release alpha to the next channel because it will be the next version 26 | if (match[1] === 'alpha') match[1] = 'next' 27 | console.log(match[1]) 28 | } else { 29 | console.log('latest') 30 | } 31 | -------------------------------------------------------------------------------- /scripts/release-notes.js: -------------------------------------------------------------------------------- 1 | // Given a version, figure out what the release notes are so that we can use this to pre-fill the 2 | // relase notes on a GitHub release for the current version. 3 | 4 | let path = require('path') 5 | let fs = require('fs') 6 | let { execSync } = require('child_process') 7 | 8 | let tag = process.argv[2] || execSync(`git describe --tags --abbrev=0`).toString().trim() 9 | let pkgPath = path.resolve( 10 | __dirname, 11 | '..', 12 | 'packages', 13 | tag.slice(0, tag.indexOf('@', 1)).replace('/', '-') 14 | ) 15 | 16 | let version = require(path.resolve(pkgPath, 'package.json')).version 17 | 18 | let changelog = fs.readFileSync(path.resolve(pkgPath, 'CHANGELOG.md'), 'utf8') 19 | let match = new RegExp( 20 | `## \\[${version}\\] - (.*)\\n\\n([\\s\\S]*?)\\n(?:(?:##\\s)|(?:\\[))`, 21 | 'g' 22 | ).exec(changelog) 23 | 24 | if (match) { 25 | let [, , notes] = match 26 | console.log(notes.trim()) 27 | } else { 28 | console.log(`Placeholder release notes for version: v${version}`) 29 | } 30 | -------------------------------------------------------------------------------- /scripts/resolve-files.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | let fastGlob = require('fast-glob') 3 | 4 | let parts = process.argv.slice(2) 5 | let [args, flags] = parts.reduce( 6 | ([args, flags], part) => { 7 | if (part.startsWith('--')) { 8 | flags[part.slice(2, part.indexOf('='))] = part.slice(part.indexOf('=') + 1) 9 | } else { 10 | args.push(part) 11 | } 12 | return [args, flags] 13 | }, 14 | [[], {}] 15 | ) 16 | 17 | flags.ignore = flags.ignore ?? '' 18 | flags.ignore = flags.ignore.split(',').filter(Boolean) 19 | 20 | console.log( 21 | fastGlob 22 | .sync(args.join('')) 23 | .filter((file) => { 24 | for (let ignore of flags.ignore) { 25 | if (file.includes(ignore)) { 26 | return false 27 | } 28 | } 29 | return true 30 | }) 31 | .join('\n') 32 | ) 33 | -------------------------------------------------------------------------------- /scripts/rewrite-imports.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | let fs = require('fs') 4 | let path = require('path') 5 | let fastGlob = require('fast-glob') 6 | 7 | console.time('Rewrote imports in') 8 | fastGlob.sync([process.argv.slice(2).join('')]).forEach((file) => { 9 | file = path.resolve(process.cwd(), file) 10 | let content = fs.readFileSync(file, 'utf8') 11 | let result = content.replace(/(import|export)([^"']*?)(["'])\.(.*?)\3/g, (full, a, b, _, d) => { 12 | // For idempotency reasons, if `.js` already exists, then we can skip this. This allows us to 13 | // run this script over and over again without adding .js files every time. 14 | if (d.endsWith('.js')) { 15 | return full 16 | } 17 | 18 | return `${a}${b}'.${d}.js'` 19 | }) 20 | if (result !== content) { 21 | fs.writeFileSync(file, result, 'utf8') 22 | } 23 | }) 24 | console.timeEnd('Rewrote imports in') 25 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | jestArgs=() 5 | 6 | # Add default arguments 7 | jestArgs+=("--passWithNoTests") 8 | 9 | # Add arguments based on environment variables 10 | if ! [ -z "$CI" ]; then 11 | jestArgs+=("--maxWorkers=4") 12 | jestArgs+=("--ci") 13 | fi 14 | 15 | # Passthrough arguments and flags 16 | jestArgs+=($@) 17 | 18 | # Execute 19 | npx jest "${jestArgs[@]}" 20 | 21 | -------------------------------------------------------------------------------- /scripts/watch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # Known variables 5 | outdir="./dist" 6 | name="headlessui" 7 | input="./src/index.ts" 8 | 9 | # Setup shared options for esbuild 10 | sharedOptions=() 11 | sharedOptions+=("--bundle") 12 | sharedOptions+=("--platform=browser") 13 | sharedOptions+=("--target=es2020") 14 | 15 | 16 | # Generate actual builds 17 | NODE_ENV=development npx esbuild $input --format=esm --outfile=$outdir/$name.esm.js --sourcemap ${sharedOptions[@]} $@ --watch 18 | 19 | --------------------------------------------------------------------------------