diff --git a/package.json b/package.json index 41efb48..5573591 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "zod": "^3.25.x" }, "dependencies": { + "@base-ui/react": "^1.2.0", "@douglasneuroinformatics/libjs": "^3.0.2", "@douglasneuroinformatics/libui-form-types": "^0.11.0", "@radix-ui/react-accordion": "^1.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d899fed..7d87006 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ settings: importers: .: dependencies: + '@base-ui/react': + specifier: ^1.2.0 + version: 1.2.0(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@douglasneuroinformatics/libjs': specifier: ^3.0.2 version: 3.1.0(neverthrow@8.2.0)(zod@3.25.56) @@ -141,7 +144,7 @@ importers: version: 3.25.56 zustand: specifier: ^5.0.3 - version: 5.0.5(@types/react@19.1.6)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)) + version: 5.0.5(@types/react@19.1.6)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)) devDependencies: '@douglasneuroinformatics/eslint-config': specifier: ^5.3.7 @@ -328,6 +331,11 @@ packages: { integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== } engines: { node: '>=6.9.0' } + '@babel/runtime@7.28.6': + resolution: + { integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== } + engines: { node: '>=6.9.0' } + '@babel/template@7.27.2': resolution: { integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== } @@ -343,6 +351,29 @@ packages: { integrity: sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q== } engines: { node: '>=6.9.0' } + '@base-ui/react@1.2.0': + resolution: + { integrity: sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw== } + engines: { node: '>=14.0.0' } + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@base-ui/utils@0.2.5': + resolution: + { integrity: sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw== } + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@bcoe/v8-coverage@1.0.2': resolution: { integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== } @@ -749,10 +780,18 @@ packages: resolution: { integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw== } + '@floating-ui/core@1.7.5': + resolution: + { integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ== } + '@floating-ui/dom@1.7.1': resolution: { integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ== } + '@floating-ui/dom@1.7.6': + resolution: + { integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ== } + '@floating-ui/react-dom@2.1.3': resolution: { integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA== } @@ -760,6 +799,17 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.8': + resolution: + { integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A== } + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: + { integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== } + '@floating-ui/utils@0.2.9': resolution: { integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== } @@ -5347,6 +5397,10 @@ packages: { integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== } engines: { node: '>=0.10.0' } + reselect@5.1.1: + resolution: + { integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== } + resolve-from@4.0.0: resolution: { integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== } @@ -5741,6 +5795,10 @@ packages: { integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A== } engines: { node: ^14.18.0 || >=16.0.0 } + tabbable@6.4.0: + resolution: + { integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== } + tailwind-merge@2.6.0: resolution: { integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA== } @@ -6074,6 +6132,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + use-sync-external-store@1.6.0: + resolution: + { integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: { integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== } @@ -6424,6 +6488,8 @@ snapshots: '@babel/runtime@7.27.6': {} + '@babel/runtime@7.28.6': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -6447,6 +6513,30 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@base-ui/react@1.2.0(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.28.6 + '@base-ui/utils': 0.2.5(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@floating-ui/react-dom': 2.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@floating-ui/utils': 0.2.11 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.6 + + '@base-ui/utils@0.2.5(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@babel/runtime': 7.28.6 + '@floating-ui/utils': 0.2.11 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.6 + '@bcoe/v8-coverage@1.0.2': {} '@colors/colors@1.5.0': @@ -6774,17 +6864,34 @@ snapshots: dependencies: '@floating-ui/utils': 0.2.9 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + '@floating-ui/dom@1.7.1': dependencies: '@floating-ui/core': 1.7.1 '@floating-ui/utils': 0.2.9 + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + '@floating-ui/react-dom@2.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@floating-ui/dom': 1.7.1 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@floating-ui/react-dom@2.1.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.9': {} '@humanfs/core@0.19.1': {} @@ -10638,6 +10745,8 @@ snapshots: require-from-string@2.0.2: {} + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -11052,6 +11161,8 @@ snapshots: dependencies: '@pkgr/core': 0.2.7 + tabbable@6.4.0: {} + tailwind-merge@2.6.0: {} tailwindcss@4.1.8: {} @@ -11317,6 +11428,10 @@ snapshots: dependencies: react: 19.1.0 + use-sync-external-store@1.6.0(react@19.1.0): + dependencies: + react: 19.1.0 + util-deprecate@1.0.2: {} validate-npm-package-license@3.0.4: @@ -11551,8 +11666,8 @@ snapshots: zod@3.25.56: {} - zustand@5.0.5(@types/react@19.1.6)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)): + zustand@5.0.5(@types/react@19.1.6)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)): optionalDependencies: '@types/react': 19.1.6 react: 19.1.0 - use-sync-external-store: 1.5.0(react@19.1.0) + use-sync-external-store: 1.6.0(react@19.1.0) diff --git a/src/components/ComboBox/ComboBox.stories.tsx b/src/components/ComboBox/ComboBox.stories.tsx new file mode 100644 index 0000000..d15c95b --- /dev/null +++ b/src/components/ComboBox/ComboBox.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { ComboBox } from './ComboBox.tsx'; + +type Story = StoryObj; + +const frameworks: string[] = ['Next.js', 'SvelteKit', 'Nuxt.js', 'Remix', 'Astro']; + +export default { + args: { + children: ( + + + + No items found. + + {(item: string) => ( + + {item} + + )} + + + + ) + }, + component: ComboBox, + tags: ['autodocs'] +} as Meta; + +export const Default: Story = {}; diff --git a/src/components/ComboBox/ComboBox.tsx b/src/components/ComboBox/ComboBox.tsx new file mode 100644 index 0000000..23a9926 --- /dev/null +++ b/src/components/ComboBox/ComboBox.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; + +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; + +import { ComboboxChip, ComboboxChips, ComboboxChipsInput } from './ComboBoxChips.tsx'; +import { ComboboxClear } from './ComboBoxClear.tsx'; +import { ComboboxCollection } from './ComboBoxCollection.tsx'; +import { ComboboxContent } from './ComboBoxContent.tsx'; +import { ComboboxEmpty } from './ComboBoxEmpty.tsx'; +import { ComboboxGroup } from './ComboBoxGroup.tsx'; +import { ComboboxInput } from './ComboBoxInput.tsx'; +import { ComboboxItem } from './ComboBoxItem.tsx'; +import { ComboboxLabel } from './ComboBoxLabel.tsx'; +import { ComboboxList } from './ComboBoxList.tsx'; +import { ComboboxSeparator } from './ComboBoxSeparator.tsx'; +import { ComboboxTrigger } from './ComboBoxTrigger.tsx'; +import { ComboboxValue } from './ComboBoxValue.tsx'; + +function useComboboxAnchor() { + return React.useRef(null); +} + +export { useComboboxAnchor }; + +export const ComboBox = Object.assign(ComboboxPrimitive.Root.bind(null), { + Chip: ComboboxChip, + Chips: ComboboxChips, + ChipsInput: ComboboxChipsInput, + Clear: ComboboxClear, + Collection: ComboboxCollection, + Content: ComboboxContent, + Empty: ComboboxEmpty, + Group: ComboboxGroup, + Input: ComboboxInput, + Item: ComboboxItem, + Label: ComboboxLabel, + List: ComboboxList, + Separator: ComboboxSeparator, + Trigger: ComboboxTrigger, + Value: ComboboxValue +}); diff --git a/src/components/ComboBox/ComboBoxChips.tsx b/src/components/ComboBox/ComboBoxChips.tsx new file mode 100644 index 0000000..0aab94c --- /dev/null +++ b/src/components/ComboBox/ComboBoxChips.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; + +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; +import { XIcon } from 'lucide-react'; + +import { cn } from '#utils'; + +import { Button } from '../Button/Button.tsx'; + +const ComboboxChips = ({ + className, + ...props +}: ComboboxPrimitive.Chips.Props & React.ComponentPropsWithRef) => { + return ( + + ); +}; + +const ComboboxChip = ({ + children, + className, + showRemove = true, + ...props +}: ComboboxPrimitive.Chip.Props & { + showRemove?: boolean; +}) => { + return ( + + {children} + {showRemove && ( + + + + } + /> + )} + + ); +}; + +const ComboboxChipsInput = ({ className, ...props }: ComboboxPrimitive.Input.Props) => { + return ( + + ); +}; + +export { ComboboxChip, ComboboxChips, ComboboxChipsInput }; diff --git a/src/components/ComboBox/ComboBoxClear.tsx b/src/components/ComboBox/ComboBoxClear.tsx new file mode 100644 index 0000000..cf156c9 --- /dev/null +++ b/src/components/ComboBox/ComboBoxClear.tsx @@ -0,0 +1,23 @@ +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; +import { XIcon } from 'lucide-react'; + +import { cn } from '#utils'; + +import { InputGroupButton } from '../InputGroup/InputGroupButton.tsx'; + +const ComboboxClear = ({ className, ...props }: ComboboxPrimitive.Clear.Props) => { + return ( + + + + } + /> + ); +}; + +export { ComboboxClear }; diff --git a/src/components/ComboBox/ComboBoxCollection.tsx b/src/components/ComboBox/ComboBoxCollection.tsx new file mode 100644 index 0000000..7d9201f --- /dev/null +++ b/src/components/ComboBox/ComboBoxCollection.tsx @@ -0,0 +1,7 @@ +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; + +const ComboboxCollection = ({ ...props }: ComboboxPrimitive.Collection.Props) => { + return ; +}; + +export { ComboboxCollection }; diff --git a/src/components/ComboBox/ComboBoxContent.tsx b/src/components/ComboBox/ComboBoxContent.tsx new file mode 100644 index 0000000..c334842 --- /dev/null +++ b/src/components/ComboBox/ComboBoxContent.tsx @@ -0,0 +1,39 @@ +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; + +import { cn } from '#utils'; + +const ComboboxContent = ({ + align = 'start', + alignOffset = 0, + anchor, + className, + side = 'bottom', + sideOffset = 6, + ...props +}: ComboboxPrimitive.Popup.Props & + Pick) => { + return ( + + + + + + ); +}; + +export { ComboboxContent }; diff --git a/src/components/ComboBox/ComboBoxEmpty.tsx b/src/components/ComboBox/ComboBoxEmpty.tsx new file mode 100644 index 0000000..b32da81 --- /dev/null +++ b/src/components/ComboBox/ComboBoxEmpty.tsx @@ -0,0 +1,18 @@ +import { Combobox as ComboboxPrimitive } from '@base-ui/react'; + +import { cn } from '#utils'; + +const ComboboxEmpty = ({ className, ...props }: ComboboxPrimitive.Empty.Props) => { + return ( +