From e1e552670064c7a4b0ba631d0a47ecf51e8e805d Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 10 Mar 2026 14:06:02 -0400 Subject: [PATCH 01/16] feat: add pakages for combo box and input group --- package.json | 1 + pnpm-lock.yaml | 121 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 3 deletions(-) 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) From 9ad4480103eae2890a07e8d56311984e3d0e593c Mon Sep 17 00:00:00 2001 From: David Roper Date: Tue, 10 Mar 2026 14:06:31 -0400 Subject: [PATCH 02/16] feat: add input group component --- src/components/InputGroup/InputGroup.tsx | 138 +++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/components/InputGroup/InputGroup.tsx diff --git a/src/components/InputGroup/InputGroup.tsx b/src/components/InputGroup/InputGroup.tsx new file mode 100644 index 0000000..a73b44f --- /dev/null +++ b/src/components/InputGroup/InputGroup.tsx @@ -0,0 +1,138 @@ +'use client'; + +import * as React from 'react'; + +import { cva } from 'class-variance-authority'; +import type { VariantProps } from 'class-variance-authority'; + +import { cn } from '#utils'; + +import { Button } from '../Button/Button.tsx'; +import { Input } from '../Input/Input.tsx'; +import { TextArea } from '../TextArea/TextArea.tsx'; + +const InputGroup = ({ className, ...props }: React.ComponentProps<'div'>) => { + return ( +
[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5', + className + )} + data-slot="input-group" + role="group" + {...props} + /> + ); +}; + +const inputGroupAddonVariants = cva( + "text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none", + { + defaultVariants: { + align: 'inline-start' + }, + variants: { + align: { + 'block-end': 'px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start', + 'block-start': + 'px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start', + 'inline-end': 'pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last', + 'inline-start': 'pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first' + } + } + } +); + +const InputGroupAddon = ({ + align = 'inline-start', + className, + ...props +}: React.ComponentProps<'div'> & VariantProps) => { + return ( +
{ + if ((e.target as HTMLElement).closest('button')) { + return; + } + e.currentTarget.parentElement?.querySelector('input')?.focus(); + }} + {...props} + /> + ); +}; + +const inputGroupButtonVariants = cva('gap-2 text-sm flex items-center shadow-none', { + defaultVariants: { + size: 'xs' + }, + variants: { + size: { + 'icon-sm': 'size-8 p-0 has-[>svg]:p-0', + 'icon-xs': 'size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0', + sm: '', + xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5" + } + } +}); + +const InputGroupButton = ({ + className, + size = 'xs', + type = 'button', + variant = 'ghost', + ...props +}: Omit, 'size'> & VariantProps) => { + return ( +