@music-vine/cadence 2.5.1 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,6 +94,11 @@ import {
94
94
  import { PriceTag } from "./price-tag";
95
95
  import { RadioGroup, RadioGroupItem } from "./radio-group";
96
96
  import { FadeAway, ScrollArea, ScrollBar } from "./scroll-area";
97
+ import {
98
+ ScrollDrum,
99
+ ScrollDrumColumn,
100
+ ScrollDrumGroup
101
+ } from "./scroll-drum";
97
102
  import {
98
103
  Select,
99
104
  SelectContent,
@@ -233,6 +238,9 @@ export {
233
238
  RadioGroupItem,
234
239
  ScrollArea,
235
240
  ScrollBar,
241
+ ScrollDrum,
242
+ ScrollDrumColumn,
243
+ ScrollDrumGroup,
236
244
  Select,
237
245
  SelectContent,
238
246
  SelectContentPopper,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/components/index.ts"],
4
- "sourcesContent": ["/**\n * Cadence UI Components\n *\n * Import components from this module:\n * @example\n * import { Button, Input } from '@music-vine/cadence/ui';\n */\n\n// Accordion\nexport {\n Accordion,\n AccordionContent,\n type AccordionContentProps,\n AccordionItem,\n type AccordionItemProps,\n type AccordionProps,\n AccordionTrigger,\n type AccordionTriggerProps,\n type AccordionVariant,\n} from \"./accordion\";\n\n// Badge\nexport { Badge, type BadgeProps, badgeVariants } from \"./badge\";\n\n// Breadcrumb\nexport {\n Breadcrumb,\n BreadcrumbEllipsis,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from \"./breadcrumb\";\n\n// Button\nexport {\n Button,\n type ButtonFontSize,\n type ButtonProps,\n type ButtonSize,\n buttonVariants,\n Loading,\n loadingVariants,\n type ResponsiveButtonFontSize,\n type ResponsiveButtonSize,\n} from \"./button\";\n\n// Card\nexport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"./card\";\n\n// Carousel\nexport {\n Carousel,\n type CarouselApi,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n useCarousel,\n} from \"./carousel\";\nexport { CarouselDots } from \"./carousel-dots\";\n\n// Checkbox\nexport { Checkbox, DummyCheckbox } from \"./checkbox\";\n\n// Context Menu\nexport {\n ContextMenu,\n ContextMenuCheckboxItem,\n ContextMenuContent,\n ContextMenuGroup,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuPortal,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuTrigger,\n} from \"./context-menu\";\n\n// Dialog\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n} from \"./dialog\";\n\n// Drawer\nexport {\n Drawer,\n DrawerClose,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerOverlay,\n DrawerPortal,\n DrawerTitle,\n DrawerTrigger,\n} from \"./drawer\";\n\n// Input\nexport {\n ClearInputButton,\n Input,\n type InputProps,\n inputVariants,\n} from \"./input\";\n\n// Label\nexport { Label } from \"./label\";\n\n// Popover\nexport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n PopoverTrigger,\n} from \"./popover\";\n\n// Price Tag\nexport { PriceTag, type PriceTagProps } from \"./price-tag\";\n\n// Radio Group\nexport { RadioGroup, RadioGroupItem } from \"./radio-group\";\n\n// Scroll Area\nexport { FadeAway, ScrollArea, ScrollBar } from \"./scroll-area\";\n\n// Select\nexport {\n Select,\n SelectContent,\n SelectContentPopper,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n} from \"./select\";\n\n// Separator\nexport { Separator } from \"./separator\";\n\n// Skeleton\nexport { Skeleton, SkeletonFragment } from \"./skeleton\";\n\n// Slider\nexport { Slider, type SliderProps, sliderVariants } from \"./slider\";\n// Stacking Card\nexport {\n StackingCard,\n StackingCardCheck,\n StackingCardContent,\n StackingCardDescription,\n StackingCardGroup,\n StackingCardHeader,\n StackingCardList,\n StackingCardListItem,\n StackingCardTitle,\n} from \"./stacking-card\";\n// Tabs\nexport { Tabs, TabsContent, TabsList, TabsTrigger } from \"./tabs\";\n// Textarea\nexport { Textarea, type TextareaProps, textareaVariants } from \"./textarea\";\n// Toast\nexport {\n type GlobalToastOptions,\n Toaster,\n type ToasterProps,\n toast,\n} from \"./toast\";\n// Toggle Button\nexport { ToggleButton, type ToggleButtonProps } from \"./toggle-button\";\n\n// Typography\nexport {\n Alpha,\n Bravo,\n Charlie,\n Delta,\n Echo,\n Foxtrot,\n Heading,\n type HeadingProps,\n headingVariants,\n List,\n ListItem,\n type ListItemProps,\n ListItemTick,\n type ListProps,\n listVariants,\n Prose,\n Text,\n type TextProps,\n textVariants,\n} from \"./typography\";\n"],
5
- "mappings": "AASA;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EAGA;AAAA,OAGK;AAGP,SAAS,OAAwB,qBAAqB;AAGtD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAG7B,SAAS,UAAU,qBAAqB;AAGxC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAGP,SAAS,aAAa;AAGtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gBAAoC;AAG7C,SAAS,YAAY,sBAAsB;AAG3C,SAAS,UAAU,YAAY,iBAAiB;AAGhD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,iBAAiB;AAG1B,SAAS,UAAU,wBAAwB;AAG3C,SAAS,QAA0B,sBAAsB;AAEzD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,MAAM,aAAa,UAAU,mBAAmB;AAEzD,SAAS,UAA8B,wBAAwB;AAE/D;AAAA,EAEE;AAAA,EAEA;AAAA,OACK;AAEP,SAAS,oBAA4C;AAGrD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;",
4
+ "sourcesContent": ["/**\n * Cadence UI Components\n *\n * Import components from this module:\n * @example\n * import { Button, Input } from '@music-vine/cadence/ui';\n */\n\n// Accordion\nexport {\n Accordion,\n AccordionContent,\n type AccordionContentProps,\n AccordionItem,\n type AccordionItemProps,\n type AccordionProps,\n AccordionTrigger,\n type AccordionTriggerProps,\n type AccordionVariant,\n} from \"./accordion\";\n\n// Badge\nexport { Badge, type BadgeProps, badgeVariants } from \"./badge\";\n\n// Breadcrumb\nexport {\n Breadcrumb,\n BreadcrumbEllipsis,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbPage,\n BreadcrumbSeparator,\n} from \"./breadcrumb\";\n\n// Button\nexport {\n Button,\n type ButtonFontSize,\n type ButtonProps,\n type ButtonSize,\n buttonVariants,\n Loading,\n loadingVariants,\n type ResponsiveButtonFontSize,\n type ResponsiveButtonSize,\n} from \"./button\";\n\n// Card\nexport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"./card\";\n\n// Carousel\nexport {\n Carousel,\n type CarouselApi,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n useCarousel,\n} from \"./carousel\";\nexport { CarouselDots } from \"./carousel-dots\";\n\n// Checkbox\nexport { Checkbox, DummyCheckbox } from \"./checkbox\";\n\n// Context Menu\nexport {\n ContextMenu,\n ContextMenuCheckboxItem,\n ContextMenuContent,\n ContextMenuGroup,\n ContextMenuItem,\n ContextMenuLabel,\n ContextMenuPortal,\n ContextMenuRadioGroup,\n ContextMenuRadioItem,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuTrigger,\n} from \"./context-menu\";\n\n// Dialog\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n} from \"./dialog\";\n\n// Drawer\nexport {\n Drawer,\n DrawerClose,\n DrawerContent,\n DrawerDescription,\n DrawerFooter,\n DrawerHeader,\n DrawerOverlay,\n DrawerPortal,\n DrawerTitle,\n DrawerTrigger,\n} from \"./drawer\";\n\n// Input\nexport {\n ClearInputButton,\n Input,\n type InputProps,\n inputVariants,\n} from \"./input\";\n\n// Label\nexport { Label } from \"./label\";\n\n// Popover\nexport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n PopoverTrigger,\n} from \"./popover\";\n\n// Price Tag\nexport { PriceTag, type PriceTagProps } from \"./price-tag\";\n\n// Radio Group\nexport { RadioGroup, RadioGroupItem } from \"./radio-group\";\n\n// Scroll Area\nexport { FadeAway, ScrollArea, ScrollBar } from \"./scroll-area\";\n\n// Scroll Drum\nexport {\n ScrollDrum,\n ScrollDrumColumn,\n type ScrollDrumColumnProps,\n ScrollDrumGroup,\n type ScrollDrumGroupProps,\n type ScrollDrumProps,\n} from \"./scroll-drum\";\n\n// Select\nexport {\n Select,\n SelectContent,\n SelectContentPopper,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n} from \"./select\";\n\n// Separator\nexport { Separator } from \"./separator\";\n\n// Skeleton\nexport { Skeleton, SkeletonFragment } from \"./skeleton\";\n\n// Slider\nexport { Slider, type SliderProps, sliderVariants } from \"./slider\";\n// Stacking Card\nexport {\n StackingCard,\n StackingCardCheck,\n StackingCardContent,\n StackingCardDescription,\n StackingCardGroup,\n StackingCardHeader,\n StackingCardList,\n StackingCardListItem,\n StackingCardTitle,\n} from \"./stacking-card\";\n// Tabs\nexport { Tabs, TabsContent, TabsList, TabsTrigger } from \"./tabs\";\n// Textarea\nexport { Textarea, type TextareaProps, textareaVariants } from \"./textarea\";\n// Toast\nexport {\n type GlobalToastOptions,\n Toaster,\n type ToasterProps,\n toast,\n} from \"./toast\";\n// Toggle Button\nexport { ToggleButton, type ToggleButtonProps } from \"./toggle-button\";\n\n// Typography\nexport {\n Alpha,\n Bravo,\n Charlie,\n Delta,\n Echo,\n Foxtrot,\n Heading,\n type HeadingProps,\n headingVariants,\n List,\n ListItem,\n type ListItemProps,\n ListItemTick,\n type ListProps,\n listVariants,\n Prose,\n Text,\n type TextProps,\n textVariants,\n} from \"./typography\";\n"],
5
+ "mappings": "AASA;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,EAGA;AAAA,OAGK;AAGP,SAAS,OAAwB,qBAAqB;AAGtD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EAIA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAG7B,SAAS,UAAU,qBAAqB;AAGxC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAGP,SAAS,aAAa;AAGtB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,gBAAoC;AAG7C,SAAS,YAAY,sBAAsB;AAG3C,SAAS,UAAU,YAAY,iBAAiB;AAGhD;AAAA,EACE;AAAA,EACA;AAAA,EAEA;AAAA,OAGK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,iBAAiB;AAG1B,SAAS,UAAU,wBAAwB;AAG3C,SAAS,QAA0B,sBAAsB;AAEzD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,MAAM,aAAa,UAAU,mBAAmB;AAEzD,SAAS,UAA8B,wBAAwB;AAE/D;AAAA,EAEE;AAAA,EAEA;AAAA,OACK;AAEP,SAAS,oBAA4C;AAGrD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;",
6
6
  "names": []
7
7
  }
@@ -29,10 +29,8 @@ const inputVariants = cva(
29
29
  "rounded-lg border border-gray-150 border-solid invalid:border-red invalid:hover:border-red focus:border-brand-primary enabled:hover:border-gray-200 enabled:focus:hover:border-brand-primary disabled:border-gray-150/60",
30
30
  // borders light
31
31
  // borders (dark)
32
- "dark:border-gray-800 dark:disabled:border-gray-900 dark:focus:border-white dark:enabled:hover:border-gray-700 dark:enabled:focus:hover:border-white dark:invalid:border-red dark:invalid:hover:border-red",
32
+ "dark:border-gray-800 dark:disabled:border-gray-900 dark:focus:border-white dark:enabled:hover:border-gray-700 dark:enabled:focus:hover:border-white dark:invalid:border-red dark:invalid:hover:border-red"
33
33
  // borders dark
34
- // default clear button
35
- "dark:[&::-webkit-search-cancel-button]:hidden"
36
34
  ],
37
35
  {
38
36
  variants: {
@@ -48,6 +46,12 @@ const inputVariants = cva(
48
46
  hasRightIcon: {
49
47
  true: "",
50
48
  false: ""
49
+ },
50
+ // Scoped to type="search" only: the Keeper password manager extension
51
+ // misidentifies inputs that style ::-webkit-search-cancel-button as
52
+ // search fields and blocks paste, so non-search inputs must not match.
53
+ type: {
54
+ search: "dark:[&::-webkit-search-cancel-button]:hidden"
51
55
  }
52
56
  },
53
57
  compoundVariants: [
@@ -119,6 +123,7 @@ const Input = ({
119
123
  size,
120
124
  hasLeftIcon: !!leftIcon,
121
125
  hasRightIcon: !!rightIcon,
126
+ type: type === "search" ? "search" : void 0,
122
127
  className
123
128
  })
124
129
  ),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/components/input.tsx"],
4
- "sourcesContent": ["/**\n * @module Input\n *\n * Styled text input with support for icons, sizes, and clear button.\n * Includes focus state management and dark mode support.\n *\n * @example\n * // Basic input\n * <Input placeholder=\"Enter your email\" type=\"email\" />\n *\n * @example\n * // With icons\n * <Input\n * leftIcon={<Search />}\n * rightIcon={<ClearInputButton onClick={clearValue} />}\n * placeholder=\"Search...\"\n * />\n *\n * @example\n * // Small size\n * <Input size=\"sm\" placeholder=\"Compact input\" />\n *\n * @example\n * // With clear button\n * const [value, setValue] = useState('');\n *\n * <Input\n * value={value}\n * onChange={(e) => setValue(e.target.value)}\n * rightIcon={\n * value && <ClearInputButton onClick={() => setValue('')} />\n * }\n * />\n */\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport type { Ref } from \"react\";\nimport { useState } from \"react\";\n\nimport { cn } from \"../lib/utils\";\n\nconst inputWrapperVariants = cva(\"relative flex w-full items-center\", {\n variants: {\n size: {\n auto: \"h-auto w-auto\",\n default: \"h-10\",\n sm: \"h-7\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n});\n\nconst inputVariants = cva(\n [\n \"w-full touch-manipulation self-stretch shadow-none outline-none outline-0 transition duration-200 ease-out disabled:cursor-not-allowed dark:disabled:cursor-not-allowed\",\n // text\n \"font-sans text-base text-black antialiased disabled:text-black/60 dark:text-white dark:disabled:text-white/60\",\n // placeholder\n \"placeholder-gray-700 placeholder:font-sans dark:placeholder-gray-200\",\n // background\n \"bg-white focus:bg-white enabled:hover:bg-white disabled:bg-white/60 dark:bg-black dark:disabled:bg-black/60 dark:focus:bg-black dark:enabled:hover:bg-black\",\n // borders (light)\n \"rounded-lg border border-gray-150 border-solid invalid:border-red invalid:hover:border-red focus:border-brand-primary enabled:hover:border-gray-200 enabled:focus:hover:border-brand-primary disabled:border-gray-150/60\", // borders light\n // borders (dark)\n \"dark:border-gray-800 dark:disabled:border-gray-900 dark:focus:border-white dark:enabled:hover:border-gray-700 dark:enabled:focus:hover:border-white dark:invalid:border-red dark:invalid:hover:border-red\", // borders dark\n // default clear button\n \"dark:[&::-webkit-search-cancel-button]:hidden\",\n ],\n {\n variants: {\n size: {\n default: \"px-4\",\n sm: \"px-3\",\n auto: \"px-4\",\n },\n hasLeftIcon: {\n true: \"\",\n false: \"\",\n },\n hasRightIcon: {\n true: \"\",\n false: \"\",\n },\n },\n compoundVariants: [\n {\n hasLeftIcon: true,\n size: \"default\",\n class: \"pl-10\",\n },\n {\n hasLeftIcon: true,\n size: \"sm\",\n class: \"pl-8\",\n },\n {\n hasRightIcon: true,\n size: \"default\",\n class: \"pr-10\",\n },\n {\n hasRightIcon: true,\n size: \"sm\",\n class: \"pr-8\",\n },\n ],\n defaultVariants: {\n size: \"default\",\n hasLeftIcon: false,\n hasRightIcon: false,\n },\n }\n);\n\ntype InputPropsWithoutSize = Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"size\"\n>;\n\ntype InputVariantsProps = VariantProps<typeof inputVariants>;\n\n// Omit hasLeftIcon and hasRightIcon from the public API\ntype PublicInputVariantsProps = Omit<\n InputVariantsProps,\n \"hasLeftIcon\" | \"hasRightIcon\"\n>;\n\n/**\n * Props for the Input component.\n * @property size - Input size: `\"default\"`, `\"sm\"`, or `\"auto\"`\n * @property htmlSize - Native HTML size attribute for input width\n * @property leftIcon - Icon element displayed on the left side\n * @property rightIcon - Icon element displayed on the right side\n */\nexport interface InputProps\n extends InputPropsWithoutSize,\n PublicInputVariantsProps {\n htmlSize?: number;\n leftIcon?: React.ReactNode;\n rightIcon?: React.ReactNode;\n ref?: Ref<HTMLInputElement>;\n}\n\nconst Input = ({\n className,\n size,\n type = \"text\",\n htmlSize,\n leftIcon,\n rightIcon,\n placeholder,\n ref,\n ...props\n}: InputProps) => {\n const [isFocused, setIsFocused] = useState(false);\n\n return (\n <div\n className={cn(inputWrapperVariants({ size }))}\n onBlur={() => {\n setTimeout(() => {\n setIsFocused(false);\n }, 50);\n }}\n >\n {!!leftIcon && (\n <SlotPrimitive.Slot\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 text-gray-950 dark:text-gray-300 [&:is(svg)]:pointer-events-none\",\n size === \"sm\" ? \"left-3 size-3\" : \"left-4 size-4\"\n )}\n >\n {leftIcon}\n </SlotPrimitive.Slot>\n )}\n <input\n className={cn(\n inputVariants({\n size,\n hasLeftIcon: !!leftIcon,\n hasRightIcon: !!rightIcon,\n className,\n })\n )}\n onFocus={(e) => {\n setIsFocused(true);\n props.onFocus?.(e);\n }}\n placeholder={isFocused ? \" \" : placeholder}\n ref={ref}\n size={htmlSize}\n type={type}\n {...props}\n />\n {!!rightIcon && (\n <SlotPrimitive.Slot\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 text-gray-950 dark:text-gray-300 [&:is(svg)]:pointer-events-none\",\n size === \"sm\" ? \"right-3 size-3\" : \"right-4 size-4\"\n )}\n >\n {rightIcon}\n </SlotPrimitive.Slot>\n )}\n </div>\n );\n};\n\ninterface ClearInputButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * Clear/reset button designed for use as Input's rightIcon.\n * Displays an X icon and handles focus states.\n */\nconst ClearInputButton = ({\n className,\n ref,\n ...props\n}: ClearInputButtonProps) => (\n <button\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 text-gray-950 dark:text-gray-300\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\",\n \"rounded-full focus-visible:outline-none dark:focus-visible:ring-[var(--focus-ring)] dark:focus-visible:ring-offset-gray-800\",\n className\n )}\n ref={ref}\n type=\"button\"\n {...props}\n >\n <X className=\"absolute inset-0 size-full\" strokeWidth={3} />\n </button>\n);\n\nexport { ClearInputButton, Input, inputVariants };\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * @module Input\n *\n * Styled text input with support for icons, sizes, and clear button.\n * Includes focus state management and dark mode support.\n *\n * @example\n * // Basic input\n * <Input placeholder=\"Enter your email\" type=\"email\" />\n *\n * @example\n * // With icons\n * <Input\n * leftIcon={<Search />}\n * rightIcon={<ClearInputButton onClick={clearValue} />}\n * placeholder=\"Search...\"\n * />\n *\n * @example\n * // Small size\n * <Input size=\"sm\" placeholder=\"Compact input\" />\n *\n * @example\n * // With clear button\n * const [value, setValue] = useState('');\n *\n * <Input\n * value={value}\n * onChange={(e) => setValue(e.target.value)}\n * rightIcon={\n * value && <ClearInputButton onClick={() => setValue('')} />\n * }\n * />\n */\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport type { Ref } from \"react\";\nimport { useState } from \"react\";\n\nimport { cn } from \"../lib/utils\";\n\nconst inputWrapperVariants = cva(\"relative flex w-full items-center\", {\n variants: {\n size: {\n auto: \"h-auto w-auto\",\n default: \"h-10\",\n sm: \"h-7\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n});\n\nconst inputVariants = cva(\n [\n \"w-full touch-manipulation self-stretch shadow-none outline-none outline-0 transition duration-200 ease-out disabled:cursor-not-allowed dark:disabled:cursor-not-allowed\",\n // text\n \"font-sans text-base text-black antialiased disabled:text-black/60 dark:text-white dark:disabled:text-white/60\",\n // placeholder\n \"placeholder-gray-700 placeholder:font-sans dark:placeholder-gray-200\",\n // background\n \"bg-white focus:bg-white enabled:hover:bg-white disabled:bg-white/60 dark:bg-black dark:disabled:bg-black/60 dark:focus:bg-black dark:enabled:hover:bg-black\",\n // borders (light)\n \"rounded-lg border border-gray-150 border-solid invalid:border-red invalid:hover:border-red focus:border-brand-primary enabled:hover:border-gray-200 enabled:focus:hover:border-brand-primary disabled:border-gray-150/60\", // borders light\n // borders (dark)\n \"dark:border-gray-800 dark:disabled:border-gray-900 dark:focus:border-white dark:enabled:hover:border-gray-700 dark:enabled:focus:hover:border-white dark:invalid:border-red dark:invalid:hover:border-red\", // borders dark\n ],\n {\n variants: {\n size: {\n default: \"px-4\",\n sm: \"px-3\",\n auto: \"px-4\",\n },\n hasLeftIcon: {\n true: \"\",\n false: \"\",\n },\n hasRightIcon: {\n true: \"\",\n false: \"\",\n },\n // Scoped to type=\"search\" only: the Keeper password manager extension\n // misidentifies inputs that style ::-webkit-search-cancel-button as\n // search fields and blocks paste, so non-search inputs must not match.\n type: {\n search: \"dark:[&::-webkit-search-cancel-button]:hidden\",\n },\n },\n compoundVariants: [\n {\n hasLeftIcon: true,\n size: \"default\",\n class: \"pl-10\",\n },\n {\n hasLeftIcon: true,\n size: \"sm\",\n class: \"pl-8\",\n },\n {\n hasRightIcon: true,\n size: \"default\",\n class: \"pr-10\",\n },\n {\n hasRightIcon: true,\n size: \"sm\",\n class: \"pr-8\",\n },\n ],\n defaultVariants: {\n size: \"default\",\n hasLeftIcon: false,\n hasRightIcon: false,\n },\n }\n);\n\ntype InputPropsWithoutSize = Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n \"size\"\n>;\n\ntype InputVariantsProps = VariantProps<typeof inputVariants>;\n\n// Omit internal-only variants from the public API. `type` is excluded so\n// consumers keep the full HTMLInputTypeAttribute union; CVA still picks up\n// the runtime value via the `type` argument passed in inputVariants().\ntype PublicInputVariantsProps = Omit<\n InputVariantsProps,\n \"hasLeftIcon\" | \"hasRightIcon\" | \"type\"\n>;\n\n/**\n * Props for the Input component.\n * @property size - Input size: `\"default\"`, `\"sm\"`, or `\"auto\"`\n * @property htmlSize - Native HTML size attribute for input width\n * @property leftIcon - Icon element displayed on the left side\n * @property rightIcon - Icon element displayed on the right side\n */\nexport interface InputProps\n extends InputPropsWithoutSize,\n PublicInputVariantsProps {\n htmlSize?: number;\n leftIcon?: React.ReactNode;\n rightIcon?: React.ReactNode;\n ref?: Ref<HTMLInputElement>;\n}\n\nconst Input = ({\n className,\n size,\n type = \"text\",\n htmlSize,\n leftIcon,\n rightIcon,\n placeholder,\n ref,\n ...props\n}: InputProps) => {\n const [isFocused, setIsFocused] = useState(false);\n\n return (\n <div\n className={cn(inputWrapperVariants({ size }))}\n onBlur={() => {\n setTimeout(() => {\n setIsFocused(false);\n }, 50);\n }}\n >\n {!!leftIcon && (\n <SlotPrimitive.Slot\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 text-gray-950 dark:text-gray-300 [&:is(svg)]:pointer-events-none\",\n size === \"sm\" ? \"left-3 size-3\" : \"left-4 size-4\"\n )}\n >\n {leftIcon}\n </SlotPrimitive.Slot>\n )}\n <input\n className={cn(\n inputVariants({\n size,\n hasLeftIcon: !!leftIcon,\n hasRightIcon: !!rightIcon,\n type: type === \"search\" ? \"search\" : undefined,\n className,\n })\n )}\n onFocus={(e) => {\n setIsFocused(true);\n props.onFocus?.(e);\n }}\n placeholder={isFocused ? \" \" : placeholder}\n ref={ref}\n size={htmlSize}\n type={type}\n {...props}\n />\n {!!rightIcon && (\n <SlotPrimitive.Slot\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 text-gray-950 dark:text-gray-300 [&:is(svg)]:pointer-events-none\",\n size === \"sm\" ? \"right-3 size-3\" : \"right-4 size-4\"\n )}\n >\n {rightIcon}\n </SlotPrimitive.Slot>\n )}\n </div>\n );\n};\n\ninterface ClearInputButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n ref?: Ref<HTMLButtonElement>;\n}\n\n/**\n * Clear/reset button designed for use as Input's rightIcon.\n * Displays an X icon and handles focus states.\n */\nconst ClearInputButton = ({\n className,\n ref,\n ...props\n}: ClearInputButtonProps) => (\n <button\n className={cn(\n \"absolute top-1/2 -translate-y-1/2 text-gray-950 dark:text-gray-300\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--focus-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-gray-100\",\n \"rounded-full focus-visible:outline-none dark:focus-visible:ring-[var(--focus-ring)] dark:focus-visible:ring-offset-gray-800\",\n className\n )}\n ref={ref}\n type=\"button\"\n {...props}\n >\n <X className=\"absolute inset-0 size-full\" strokeWidth={3} />\n </button>\n);\n\nexport { ClearInputButton, Input, inputVariants };\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,355 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import {
3
+ Children,
4
+ Fragment,
5
+ useCallback,
6
+ useEffect,
7
+ useId,
8
+ useRef,
9
+ useState
10
+ } from "react";
11
+ import { cn } from "../lib/utils";
12
+ const DEFAULT_ITEM_HEIGHT = 52;
13
+ const DEFAULT_VISIBLE = 3;
14
+ const DEFAULT_MAX_CHARS = 4;
15
+ const SNAP_DURATION_MS = 280;
16
+ const WHEEL_SETTLE_MS = 110;
17
+ const COAST_FACTOR = 220 * 0.55;
18
+ const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
19
+ const pmod = (n, m) => (n % m + m) % m;
20
+ const ScrollDrum = ({
21
+ items,
22
+ value,
23
+ onChange,
24
+ itemHeight = DEFAULT_ITEM_HEIGHT,
25
+ visibleCount = DEFAULT_VISIBLE,
26
+ loop: loopProp = true,
27
+ maxChars = DEFAULT_MAX_CHARS,
28
+ ariaLabel,
29
+ className
30
+ }) => {
31
+ const uid = useId();
32
+ const optionId = (idx) => `${uid}-opt-${idx}`;
33
+ const containerRef = useRef(null);
34
+ const animRef = useRef(0);
35
+ const wheelTimer = useRef(null);
36
+ const dragRef = useRef({
37
+ active: false,
38
+ startY: 0,
39
+ startIndex: 0,
40
+ lastY: 0,
41
+ lastT: 0,
42
+ vy: 0,
43
+ moved: false
44
+ });
45
+ const N = items.length;
46
+ const loop = loopProp && N > 1;
47
+ const truncate = useCallback(
48
+ (label) => label.length > maxChars ? label.slice(0, maxChars) : label,
49
+ [maxChars]
50
+ );
51
+ const initialIndex = Math.max(0, items.indexOf(value));
52
+ const [floatIndex, setFloatIndex] = useState(initialIndex);
53
+ const floatIndexRef = useRef(floatIndex);
54
+ useEffect(() => {
55
+ floatIndexRef.current = floatIndex;
56
+ }, [floatIndex]);
57
+ const resolveIndex = useCallback(
58
+ (f) => {
59
+ if (loop) return pmod(Math.round(f), N);
60
+ return clamp(Math.round(f), 0, N - 1);
61
+ },
62
+ [loop, N]
63
+ );
64
+ const animateTo = useCallback(
65
+ (targetFloat) => {
66
+ cancelAnimationFrame(animRef.current);
67
+ const start = performance.now();
68
+ const from = floatIndexRef.current;
69
+ const ease = (t) => 1 - (1 - t) ** 3;
70
+ const step = () => {
71
+ const t = clamp((performance.now() - start) / SNAP_DURATION_MS, 0, 1);
72
+ const v = from + (targetFloat - from) * ease(t);
73
+ setFloatIndex(v);
74
+ if (t < 1) {
75
+ animRef.current = requestAnimationFrame(step);
76
+ } else {
77
+ const final = loop ? pmod(targetFloat, N) : clamp(targetFloat, 0, N - 1);
78
+ setFloatIndex(final);
79
+ const idx = resolveIndex(final);
80
+ const next = items[idx];
81
+ if (next !== void 0 && next !== value) onChange?.(next, idx);
82
+ }
83
+ };
84
+ animRef.current = requestAnimationFrame(step);
85
+ },
86
+ [items, loop, N, onChange, resolveIndex, value]
87
+ );
88
+ const targetIdx = N > 0 ? Math.max(0, items.indexOf(value)) : 0;
89
+ const lastSyncedTargetIdx = useRef(targetIdx);
90
+ useEffect(() => {
91
+ if (targetIdx === lastSyncedTargetIdx.current) return;
92
+ lastSyncedTargetIdx.current = targetIdx;
93
+ if (N === 0) return;
94
+ const cur = floatIndexRef.current;
95
+ let target = targetIdx;
96
+ if (loop) {
97
+ const cycles = Math.round((cur - targetIdx) / N);
98
+ target = targetIdx + cycles * N;
99
+ }
100
+ if (Math.abs(target - cur) > 1e-3) animateTo(target);
101
+ }, [targetIdx]);
102
+ useEffect(() => {
103
+ const el = containerRef.current;
104
+ if (!el) return;
105
+ const onWheel = (e) => {
106
+ e.preventDefault();
107
+ cancelAnimationFrame(animRef.current);
108
+ const delta = e.deltaY / itemHeight;
109
+ const raw = floatIndexRef.current + delta;
110
+ setFloatIndex(loop ? raw : clamp(raw, 0, N - 1));
111
+ if (wheelTimer.current) clearTimeout(wheelTimer.current);
112
+ wheelTimer.current = setTimeout(() => {
113
+ const cur = floatIndexRef.current;
114
+ const target = loop ? Math.round(cur) : clamp(Math.round(cur), 0, N - 1);
115
+ animateTo(target);
116
+ }, WHEEL_SETTLE_MS);
117
+ };
118
+ el.addEventListener("wheel", onWheel, { passive: false });
119
+ return () => {
120
+ el.removeEventListener("wheel", onWheel);
121
+ if (wheelTimer.current) clearTimeout(wheelTimer.current);
122
+ };
123
+ }, [animateTo, N, itemHeight, loop]);
124
+ useEffect(
125
+ () => () => {
126
+ cancelAnimationFrame(animRef.current);
127
+ },
128
+ []
129
+ );
130
+ const onPointerDown = (e) => {
131
+ if (e.pointerType === "mouse" && e.button !== 0) return;
132
+ cancelAnimationFrame(animRef.current);
133
+ containerRef.current?.setPointerCapture(e.pointerId);
134
+ dragRef.current = {
135
+ active: true,
136
+ startY: e.clientY,
137
+ startIndex: floatIndexRef.current,
138
+ lastY: e.clientY,
139
+ lastT: performance.now(),
140
+ vy: 0,
141
+ moved: false
142
+ };
143
+ };
144
+ const onPointerMove = (e) => {
145
+ const d = dragRef.current;
146
+ if (!d.active) return;
147
+ const dy = e.clientY - d.startY;
148
+ if (Math.abs(dy) > 3) d.moved = true;
149
+ const raw = d.startIndex - dy / itemHeight;
150
+ setFloatIndex(loop ? raw : clamp(raw, 0, N - 1));
151
+ const now = performance.now();
152
+ const dt = Math.max(1, now - d.lastT);
153
+ d.vy = (e.clientY - d.lastY) / dt;
154
+ d.lastY = e.clientY;
155
+ d.lastT = now;
156
+ };
157
+ const onPointerUp = (e) => {
158
+ const d = dragRef.current;
159
+ if (!d.active) return;
160
+ d.active = false;
161
+ try {
162
+ containerRef.current?.releasePointerCapture(e.pointerId);
163
+ } catch {
164
+ }
165
+ const coastPx = d.vy * COAST_FACTOR;
166
+ const projected = floatIndexRef.current - coastPx / itemHeight;
167
+ const target = loop ? Math.round(projected) : clamp(Math.round(projected), 0, N - 1);
168
+ animateTo(target);
169
+ };
170
+ const onKeyDown = (e) => {
171
+ let target = Math.round(floatIndexRef.current);
172
+ if (e.key === "ArrowUp") target -= 1;
173
+ else if (e.key === "ArrowDown") target += 1;
174
+ else if (e.key === "PageUp") target -= 5;
175
+ else if (e.key === "PageDown") target += 5;
176
+ else if (e.key === "Home") {
177
+ const cur = Math.round(floatIndexRef.current);
178
+ target = loop ? cur - pmod(cur, N) : 0;
179
+ } else if (e.key === "End") {
180
+ const cur = Math.round(floatIndexRef.current);
181
+ target = loop ? cur - pmod(cur, N) + N - 1 : N - 1;
182
+ } else return;
183
+ e.preventDefault();
184
+ if (!loop) target = clamp(target, 0, N - 1);
185
+ animateTo(target);
186
+ };
187
+ const halfVisible = Math.floor(visibleCount / 2);
188
+ const windowRadius = halfVisible + 2;
189
+ const centerInt = Math.round(floatIndex);
190
+ const visibleItems = [];
191
+ for (let off = -windowRadius; off <= windowRadius; off++) {
192
+ const rawIdx = centerInt + off;
193
+ if (!loop && (rawIdx < 0 || rawIdx >= N)) continue;
194
+ const itemIdx = pmod(rawIdx, N);
195
+ const label = items[itemIdx];
196
+ if (label === void 0) continue;
197
+ visibleItems.push({
198
+ key: String(rawIdx),
199
+ itemIdx,
200
+ offsetSlot: rawIdx,
201
+ label
202
+ });
203
+ }
204
+ const rowOffset = (slot) => (slot - floatIndex) * itemHeight;
205
+ const selectedIndex = N > 0 ? resolveIndex(floatIndex) : 0;
206
+ if (N === 0) {
207
+ return /* @__PURE__ */ jsx(
208
+ "div",
209
+ {
210
+ "aria-label": ariaLabel,
211
+ className: cn(
212
+ "relative h-full w-full overflow-hidden bg-white dark:bg-gray-900",
213
+ className
214
+ ),
215
+ role: "listbox"
216
+ }
217
+ );
218
+ }
219
+ return /* @__PURE__ */ jsxs(
220
+ "div",
221
+ {
222
+ "aria-activedescendant": optionId(selectedIndex),
223
+ "aria-label": ariaLabel,
224
+ "aria-orientation": "vertical",
225
+ className: cn(
226
+ "relative h-full w-full cursor-grab touch-none select-none overflow-hidden bg-white outline-none active:cursor-grabbing focus-visible:[box-shadow:inset_0_0_0_2px_var(--brand-primary)] dark:bg-gray-900",
227
+ className
228
+ ),
229
+ onKeyDown,
230
+ onPointerCancel: onPointerUp,
231
+ onPointerDown,
232
+ onPointerMove,
233
+ onPointerUp,
234
+ ref: containerRef,
235
+ role: "listbox",
236
+ style: { "--sd-item-h": `${itemHeight}px` },
237
+ tabIndex: 0,
238
+ children: [
239
+ /* @__PURE__ */ jsx(
240
+ "div",
241
+ {
242
+ "aria-hidden": "true",
243
+ className: "pointer-events-none absolute inset-x-0 top-[-1px] z-[2] h-9 bg-gradient-to-b from-white from-[12%] to-transparent dark:from-gray-900"
244
+ }
245
+ ),
246
+ /* @__PURE__ */ jsx(
247
+ "div",
248
+ {
249
+ "aria-hidden": "true",
250
+ className: "pointer-events-none absolute inset-x-0 bottom-[-1px] z-[2] h-9 bg-gradient-to-t from-white from-[12%] to-transparent dark:from-gray-900"
251
+ }
252
+ ),
253
+ /* @__PURE__ */ jsx(
254
+ "div",
255
+ {
256
+ "aria-hidden": "true",
257
+ className: "pointer-events-none absolute inset-x-2 top-1/2 z-0 h-[41px] -translate-y-1/2 rounded-sm border border-brand-primary bg-brand-secondary"
258
+ }
259
+ ),
260
+ /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0", children: visibleItems.map(({ key, itemIdx, offsetSlot, label }) => {
261
+ const isSelected = itemIdx === selectedIndex && Math.abs(offsetSlot - floatIndex) <= 0.5;
262
+ return /* @__PURE__ */ jsx(
263
+ "button",
264
+ {
265
+ "aria-selected": isSelected,
266
+ className: cn(
267
+ "pointer-events-auto absolute top-1/2 left-1/2 z-[1] m-0 box-border flex h-[var(--sd-item-h,52px)] w-full cursor-pointer items-center justify-center overflow-hidden border-0 bg-transparent px-1.5 py-0 transition-colors duration-150",
268
+ isSelected ? "text-brand-primary-hover dark:text-white" : "text-gray-600 dark:text-gray-300"
269
+ ),
270
+ id: isSelected ? optionId(itemIdx) : void 0,
271
+ onClick: () => {
272
+ if (dragRef.current.moved) return;
273
+ animateTo(offsetSlot);
274
+ },
275
+ role: "option",
276
+ style: {
277
+ transform: `translate3d(-50%, calc(-50% + ${rowOffset(offsetSlot)}px), 0)`
278
+ },
279
+ tabIndex: -1,
280
+ type: "button",
281
+ children: /* @__PURE__ */ jsx("span", { className: "inline-block whitespace-nowrap text-center font-sans font-semibold text-[30px] leading-9 tracking-[-1px] [font-feature-settings:'ss03']", children: truncate(label) })
282
+ },
283
+ key
284
+ );
285
+ }) })
286
+ ]
287
+ }
288
+ );
289
+ };
290
+ const ScrollDrumGroup = ({
291
+ children,
292
+ separator = ":",
293
+ showSeparators = true,
294
+ className
295
+ }) => {
296
+ const cols = Children.toArray(children);
297
+ return /* @__PURE__ */ jsx(
298
+ "div",
299
+ {
300
+ className: cn(
301
+ "inline-flex flex-row items-center justify-center rounded-lg bg-gray-100 px-8 pt-6 pb-4 dark:bg-gray-800",
302
+ className
303
+ ),
304
+ children: cols.map((col, idx) => (
305
+ // biome-ignore lint/suspicious/noArrayIndexKey: columns are static and not reordered
306
+ /* @__PURE__ */ jsxs(Fragment, { children: [
307
+ col,
308
+ idx < cols.length - 1 && /* @__PURE__ */ jsx(
309
+ "div",
310
+ {
311
+ "aria-hidden": "true",
312
+ className: "flex h-[120px] w-8 flex-none items-center justify-center self-start",
313
+ children: showSeparators && /* @__PURE__ */ jsx("span", { className: "text-center font-sans font-semibold text-[36px] text-gray-600 leading-10 tracking-[-1.5px] [font-feature-settings:'ss03'] dark:text-gray-400", children: separator })
314
+ }
315
+ )
316
+ ] }, idx)
317
+ ))
318
+ }
319
+ );
320
+ };
321
+ const ScrollDrumColumn = ({
322
+ label,
323
+ width,
324
+ maxChars = DEFAULT_MAX_CHARS,
325
+ children,
326
+ className
327
+ }) => {
328
+ const computed = width ?? Math.max(72, 24 + maxChars * 16);
329
+ return /* @__PURE__ */ jsxs(
330
+ "div",
331
+ {
332
+ className: cn(
333
+ "flex flex-none flex-col gap-2 p-0",
334
+ className
335
+ ),
336
+ style: { width: computed },
337
+ children: [
338
+ /* @__PURE__ */ jsx("div", { className: "flex h-[120px] flex-none items-center justify-center bg-white dark:bg-gray-900", children }),
339
+ label !== void 0 && /* @__PURE__ */ jsx(
340
+ "div",
341
+ {
342
+ className: "h-6 text-center font-sans font-semibold text-base text-gray-600 leading-6 [font-feature-settings:'ss03'] dark:text-gray-300",
343
+ children: label || "\xA0"
344
+ }
345
+ )
346
+ ]
347
+ }
348
+ );
349
+ };
350
+ export {
351
+ ScrollDrum,
352
+ ScrollDrumColumn,
353
+ ScrollDrumGroup
354
+ };
355
+ //# sourceMappingURL=scroll-drum.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/components/scroll-drum.tsx"],
4
+ "sourcesContent": ["/**\n * @module ScrollDrum\n *\n * iOS-style picker columns for selecting a value from a constrained list \u2014\n * durations, time, dates, BPM, etc. Supports wheel/trackpad scrolling, mouse\n * and touch drag with momentum, click-to-center, full keyboard control, and\n * optional infinite looping.\n *\n * @example\n * // Single drum: count-in bars\n * const [val, setVal] = useState(\"8\");\n * const items = Array.from({ length: 31 }, (_, i) => String(i));\n *\n * <ScrollDrumGroup>\n * <ScrollDrumColumn label=\"bars\">\n * <ScrollDrum items={items} value={val} onChange={setVal} ariaLabel=\"Bars\" />\n * </ScrollDrumColumn>\n * </ScrollDrumGroup>\n *\n * @example\n * // 12-hour time picker with separator\n * <ScrollDrumGroup separator=\":\">\n * <ScrollDrumColumn label=\"hour\">\n * <ScrollDrum items={hours} value={h} onChange={setH} ariaLabel=\"Hour\" />\n * </ScrollDrumColumn>\n * <ScrollDrumColumn label=\"min\">\n * <ScrollDrum items={minutes} value={m} onChange={setM} ariaLabel=\"Minute\" />\n * </ScrollDrumColumn>\n * <ScrollDrumColumn label=\"\">\n * <ScrollDrum items={[\"AM\", \"PM\"]} value={p} onChange={setP} loop={false} />\n * </ScrollDrumColumn>\n * </ScrollDrumGroup>\n */\nimport {\n Children,\n Fragment,\n type PointerEvent as ReactPointerEvent,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n useCallback,\n useEffect,\n useId,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"../lib/utils\";\n\nconst DEFAULT_ITEM_HEIGHT = 52;\nconst DEFAULT_VISIBLE = 3;\nconst DEFAULT_MAX_CHARS = 4;\n\nconst SNAP_DURATION_MS = 280;\nconst WHEEL_SETTLE_MS = 110;\nconst COAST_FACTOR = 220 * 0.55;\n\nconst clamp = (n: number, lo: number, hi: number): number =>\n Math.max(lo, Math.min(hi, n));\n\n// Positive modulo (handles negatives) \u2014 required for stable wrap math.\nconst pmod = (n: number, m: number): number => ((n % m) + m) % m;\n\ninterface ScrollDrumProps {\n /** Ordered list of selectable strings. Order is the visual order. */\n items: string[];\n /** Currently selected item. Must exist in `items`. */\n value: string;\n /** Fires with `(newValue, newIndex)` when the user lands on a new row. */\n onChange?: (value: string, index: number) => void;\n /** Vertical pixel size of one row slot. Defaults to 52. */\n itemHeight?: number;\n /** How many rows are visible at once (odd numbers center cleanly). */\n visibleCount?: number;\n /** When `true` (default), scrolling past the end wraps to the start. */\n loop?: boolean;\n /** Hard character cap per item. Long labels are truncated for layout. */\n maxChars?: number;\n /** Accessible name for the listbox. */\n ariaLabel?: string;\n /** Additional class names merged onto the drum element. */\n className?: string;\n}\n\n/**\n * A single scrollable column. Designed to be wrapped in a {@link ScrollDrumColumn}\n * and grouped via {@link ScrollDrumGroup}.\n */\nconst ScrollDrum = ({\n items,\n value,\n onChange,\n itemHeight = DEFAULT_ITEM_HEIGHT,\n visibleCount = DEFAULT_VISIBLE,\n loop: loopProp = true,\n maxChars = DEFAULT_MAX_CHARS,\n ariaLabel,\n className,\n}: ScrollDrumProps): ReactNode => {\n const uid = useId();\n const optionId = (idx: number): string => `${uid}-opt-${idx}`;\n const containerRef = useRef<HTMLDivElement | null>(null);\n const animRef = useRef<number>(0);\n const wheelTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const dragRef = useRef({\n active: false,\n startY: 0,\n startIndex: 0,\n lastY: 0,\n lastT: 0,\n vy: 0,\n moved: false,\n });\n\n const N = items.length;\n // Looping past zero items would call pmod(n, 0) and yield NaN, breaking the\n // render math. Force non-looping internally when there's nothing to loop\n // through; the render bails on N === 0.\n const loop = loopProp && N > 1;\n\n const truncate = useCallback(\n (label: string): string =>\n label.length > maxChars ? label.slice(0, maxChars) : label,\n [maxChars]\n );\n\n const initialIndex = Math.max(0, items.indexOf(value));\n const [floatIndex, setFloatIndex] = useState<number>(initialIndex);\n const floatIndexRef = useRef<number>(floatIndex);\n useEffect(() => {\n floatIndexRef.current = floatIndex;\n }, [floatIndex]);\n\n const resolveIndex = useCallback(\n (f: number): number => {\n if (loop) return pmod(Math.round(f), N);\n return clamp(Math.round(f), 0, N - 1);\n },\n [loop, N]\n );\n\n const animateTo = useCallback(\n (targetFloat: number): void => {\n cancelAnimationFrame(animRef.current);\n const start = performance.now();\n const from = floatIndexRef.current;\n const ease = (t: number): number => 1 - (1 - t) ** 3;\n const step = (): void => {\n const t = clamp((performance.now() - start) / SNAP_DURATION_MS, 0, 1);\n const v = from + (targetFloat - from) * ease(t);\n setFloatIndex(v);\n if (t < 1) {\n animRef.current = requestAnimationFrame(step);\n } else {\n const final = loop\n ? pmod(targetFloat, N)\n : clamp(targetFloat, 0, N - 1);\n setFloatIndex(final);\n const idx = resolveIndex(final);\n const next = items[idx];\n if (next !== undefined && next !== value) onChange?.(next, idx);\n }\n };\n animRef.current = requestAnimationFrame(step);\n },\n [items, loop, N, onChange, resolveIndex, value]\n );\n\n // External value -> internal float, animated on the shortest path. Tracked\n // by value's resolved index rather than the items reference, so an unstable\n // `items` array (fresh ref every render) doesn't cancel an in-flight\n // animation, but a real reorder (where `value` lands at a different index)\n // still resyncs the drum.\n const targetIdx = N > 0 ? Math.max(0, items.indexOf(value)) : 0;\n const lastSyncedTargetIdx = useRef<number>(targetIdx);\n useEffect(() => {\n if (targetIdx === lastSyncedTargetIdx.current) return;\n lastSyncedTargetIdx.current = targetIdx;\n if (N === 0) return;\n const cur = floatIndexRef.current;\n let target = targetIdx;\n if (loop) {\n const cycles = Math.round((cur - targetIdx) / N);\n target = targetIdx + cycles * N;\n }\n if (Math.abs(target - cur) > 0.001) animateTo(target);\n // animateTo intentionally omitted to avoid retriggering on every render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [targetIdx]);\n\n // Wheel: native non-passive listener so we can preventDefault.\n useEffect(() => {\n const el = containerRef.current;\n if (!el) return;\n const onWheel = (e: WheelEvent): void => {\n e.preventDefault();\n cancelAnimationFrame(animRef.current);\n const delta = e.deltaY / itemHeight;\n const raw = floatIndexRef.current + delta;\n setFloatIndex(loop ? raw : clamp(raw, 0, N - 1));\n if (wheelTimer.current) clearTimeout(wheelTimer.current);\n wheelTimer.current = setTimeout(() => {\n const cur = floatIndexRef.current;\n const target = loop\n ? Math.round(cur)\n : clamp(Math.round(cur), 0, N - 1);\n animateTo(target);\n }, WHEEL_SETTLE_MS);\n };\n el.addEventListener(\"wheel\", onWheel, { passive: false });\n return () => {\n el.removeEventListener(\"wheel\", onWheel);\n if (wheelTimer.current) clearTimeout(wheelTimer.current);\n };\n }, [animateTo, N, itemHeight, loop]);\n\n useEffect(\n () => () => {\n cancelAnimationFrame(animRef.current);\n },\n []\n );\n\n const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>): void => {\n if (e.pointerType === \"mouse\" && e.button !== 0) return;\n cancelAnimationFrame(animRef.current);\n containerRef.current?.setPointerCapture(e.pointerId);\n dragRef.current = {\n active: true,\n startY: e.clientY,\n startIndex: floatIndexRef.current,\n lastY: e.clientY,\n lastT: performance.now(),\n vy: 0,\n moved: false,\n };\n };\n\n const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>): void => {\n const d = dragRef.current;\n if (!d.active) return;\n const dy = e.clientY - d.startY;\n if (Math.abs(dy) > 3) d.moved = true;\n const raw = d.startIndex - dy / itemHeight;\n setFloatIndex(loop ? raw : clamp(raw, 0, N - 1));\n const now = performance.now();\n const dt = Math.max(1, now - d.lastT);\n d.vy = (e.clientY - d.lastY) / dt;\n d.lastY = e.clientY;\n d.lastT = now;\n };\n\n const onPointerUp = (e: ReactPointerEvent<HTMLDivElement>): void => {\n const d = dragRef.current;\n if (!d.active) return;\n d.active = false;\n try {\n containerRef.current?.releasePointerCapture(e.pointerId);\n } catch {\n /* pointer may already be released */\n }\n const coastPx = d.vy * COAST_FACTOR;\n const projected = floatIndexRef.current - coastPx / itemHeight;\n const target = loop\n ? Math.round(projected)\n : clamp(Math.round(projected), 0, N - 1);\n animateTo(target);\n };\n\n const onKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>): void => {\n let target = Math.round(floatIndexRef.current);\n if (e.key === \"ArrowUp\") target -= 1;\n else if (e.key === \"ArrowDown\") target += 1;\n else if (e.key === \"PageUp\") target -= 5;\n else if (e.key === \"PageDown\") target += 5;\n else if (e.key === \"Home\") {\n const cur = Math.round(floatIndexRef.current);\n target = loop ? cur - pmod(cur, N) : 0;\n } else if (e.key === \"End\") {\n const cur = Math.round(floatIndexRef.current);\n target = loop ? cur - pmod(cur, N) + N - 1 : N - 1;\n } else return;\n e.preventDefault();\n if (!loop) target = clamp(target, 0, N - 1);\n animateTo(target);\n };\n\n const halfVisible = Math.floor(visibleCount / 2);\n const windowRadius = halfVisible + 2;\n const centerInt = Math.round(floatIndex);\n\n const visibleItems: Array<{\n key: string;\n itemIdx: number;\n offsetSlot: number;\n label: string;\n }> = [];\n for (let off = -windowRadius; off <= windowRadius; off++) {\n const rawIdx = centerInt + off;\n if (!loop && (rawIdx < 0 || rawIdx >= N)) continue;\n const itemIdx = pmod(rawIdx, N);\n const label = items[itemIdx];\n if (label === undefined) continue;\n visibleItems.push({\n key: String(rawIdx),\n itemIdx,\n offsetSlot: rawIdx,\n label,\n });\n }\n\n const rowOffset = (slot: number): number => (slot - floatIndex) * itemHeight;\n const selectedIndex = N > 0 ? resolveIndex(floatIndex) : 0;\n\n if (N === 0) {\n return (\n <div\n aria-label={ariaLabel}\n className={cn(\n \"relative h-full w-full overflow-hidden bg-white dark:bg-gray-900\",\n className\n )}\n role=\"listbox\"\n />\n );\n }\n\n return (\n <div\n aria-activedescendant={optionId(selectedIndex)}\n aria-label={ariaLabel}\n aria-orientation=\"vertical\"\n className={cn(\n \"relative h-full w-full cursor-grab touch-none select-none overflow-hidden bg-white outline-none active:cursor-grabbing focus-visible:[box-shadow:inset_0_0_0_2px_var(--brand-primary)] dark:bg-gray-900\",\n className\n )}\n onKeyDown={onKeyDown}\n onPointerCancel={onPointerUp}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n ref={containerRef}\n role=\"listbox\"\n style={{ \"--sd-item-h\": `${itemHeight}px` } as React.CSSProperties}\n tabIndex={0}\n >\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-0 top-[-1px] z-[2] h-9 bg-gradient-to-b from-white from-[12%] to-transparent dark:from-gray-900\"\n />\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-0 bottom-[-1px] z-[2] h-9 bg-gradient-to-t from-white from-[12%] to-transparent dark:from-gray-900\"\n />\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute inset-x-2 top-1/2 z-0 h-[41px] -translate-y-1/2 rounded-sm border border-brand-primary bg-brand-secondary\"\n />\n\n <div className=\"pointer-events-none absolute inset-0\">\n {visibleItems.map(({ key, itemIdx, offsetSlot, label }) => {\n const isSelected =\n itemIdx === selectedIndex &&\n Math.abs(offsetSlot - floatIndex) <= 0.5;\n return (\n <button\n aria-selected={isSelected}\n className={cn(\n \"pointer-events-auto absolute top-1/2 left-1/2 z-[1] m-0 box-border flex h-[var(--sd-item-h,52px)] w-full cursor-pointer items-center justify-center overflow-hidden border-0 bg-transparent px-1.5 py-0 transition-colors duration-150\",\n isSelected\n ? \"text-brand-primary-hover dark:text-white\"\n : \"text-gray-600 dark:text-gray-300\"\n )}\n id={isSelected ? optionId(itemIdx) : undefined}\n key={key}\n onClick={() => {\n if (dragRef.current.moved) return;\n animateTo(offsetSlot);\n }}\n role=\"option\"\n style={{\n transform: `translate3d(-50%, calc(-50% + ${rowOffset(offsetSlot)}px), 0)`,\n }}\n tabIndex={-1}\n type=\"button\"\n >\n <span className=\"inline-block whitespace-nowrap text-center font-sans font-semibold text-[30px] leading-9 tracking-[-1px] [font-feature-settings:'ss03']\">\n {truncate(label)}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n );\n};\n\ninterface ScrollDrumGroupProps {\n /** One or more {@link ScrollDrumColumn} children. */\n children: ReactNode;\n /** Glyph rendered between adjacent columns. Ignored when `showSeparators` is false. */\n separator?: string;\n /** When `false`, the gutter between columns is preserved but no glyph is rendered. */\n showSeparators?: boolean;\n /** Additional class names merged onto the group container. */\n className?: string;\n}\n\n/**\n * Gray container that holds one or more {@link ScrollDrumColumn} children.\n * Optionally renders a glyph between columns (e.g. `:` for time, `/` for date).\n */\nconst ScrollDrumGroup = ({\n children,\n separator = \":\",\n showSeparators = true,\n className,\n}: ScrollDrumGroupProps): ReactNode => {\n const cols = Children.toArray(children);\n return (\n <div\n className={cn(\n \"inline-flex flex-row items-center justify-center rounded-lg bg-gray-100 px-8 pt-6 pb-4 dark:bg-gray-800\",\n className\n )}\n >\n {cols.map((col, idx) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: columns are static and not reordered\n <Fragment key={idx}>\n {col}\n {idx < cols.length - 1 && (\n <div\n aria-hidden=\"true\"\n className=\"flex h-[120px] w-8 flex-none items-center justify-center self-start\"\n >\n {showSeparators && (\n <span className=\"text-center font-sans font-semibold text-[36px] text-gray-600 leading-10 tracking-[-1.5px] [font-feature-settings:'ss03'] dark:text-gray-400\">\n {separator}\n </span>\n )}\n </div>\n )}\n </Fragment>\n ))}\n </div>\n );\n};\n\ninterface ScrollDrumColumnProps {\n /** Caption rendered below the drum slab. Pass `\"\"` for an invisible spacer. */\n label?: string;\n /** Explicit pixel width override. When omitted, scales with `maxChars`. */\n width?: number;\n /** Hard character cap that drives the auto-computed column width. */\n maxChars?: number;\n /** Should be a single {@link ScrollDrum}. */\n children: ReactNode;\n /** Additional class names merged onto the column wrapper. */\n className?: string;\n}\n\n/**\n * Pairs a {@link ScrollDrum} with an optional caption underneath. When `width`\n * is omitted the column auto-sizes via `Math.max(72, 24 + maxChars * 16)`, so\n * a 4-char drum (e.g. `\"2026\"`) lands at 88px and a 2-char drum stays at the\n * 72px floor.\n */\nconst ScrollDrumColumn = ({\n label,\n width,\n maxChars = DEFAULT_MAX_CHARS,\n children,\n className,\n}: ScrollDrumColumnProps): ReactNode => {\n const computed = width ?? Math.max(72, 24 + maxChars * 16);\n return (\n <div\n className={cn(\n \"flex flex-none flex-col gap-2 p-0\",\n className\n )}\n style={{ width: computed }}\n >\n <div className=\"flex h-[120px] flex-none items-center justify-center bg-white dark:bg-gray-900\">\n {children}\n </div>\n {label !== undefined && (\n <div\n className=\"h-6 text-center font-sans font-semibold text-base text-gray-600 leading-6 [font-feature-settings:'ss03'] dark:text-gray-300\"\n >\n {label || \"\u00A0\"}\n </div>\n )}\n </div>\n );\n};\n\nexport { ScrollDrum, ScrollDrumGroup, ScrollDrumColumn };\nexport type { ScrollDrumProps, ScrollDrumGroupProps, ScrollDrumColumnProps };\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -11,4 +11,6 @@ body {
11
11
  .theme-dark {
12
12
  background-color: #151a20;
13
13
  color: #fff;
14
+ -webkit-font-smoothing: antialiased;
15
+ -moz-osx-font-smoothing: grayscale;
14
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@music-vine/cadence",
3
- "version": "2.5.1",
3
+ "version": "2.6.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -132,14 +132,14 @@
132
132
  "jsdom": "^26.1.0",
133
133
  "pixelmatch": "^7.1.0",
134
134
  "pngjs": "^7.0.0",
135
- "postcss": "^8.4.49",
135
+ "postcss": "^8.5.10",
136
136
  "rimraf": "^6.0.1",
137
137
  "storybook": "^10.2.10",
138
138
  "tailwindcss": "^4.1.18",
139
139
  "tailwindcss-v3": "npm:tailwindcss@^3.4.18",
140
140
  "tsx": "^4.21.0",
141
141
  "typescript": "^5.2.2",
142
- "vite": "^7.3.2",
142
+ "vite": "^8.0.7",
143
143
  "vitest": "^3.2.4"
144
144
  },
145
145
  "scripts": {