@fanvue/ui 3.15.0 → 3.17.0
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.
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs +16 -89
- package/dist/cjs/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs +54 -0
- package/dist/cjs/components/AudioRecordButton/AudioRecordButton.cjs.map +1 -0
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs +5 -5
- package/dist/cjs/components/AudioUpload/AudioUpload.cjs.map +1 -1
- package/dist/cjs/components/ChatInput/ChatInput.cjs +6 -11
- package/dist/cjs/components/ChatInput/ChatInput.cjs.map +1 -1
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs +228 -0
- package/dist/cjs/components/ChatMessage/ChatMessage.cjs.map +1 -0
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs +160 -71
- package/dist/cjs/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs +52 -0
- package/dist/cjs/components/Icons/DenseGridViewIcon.cjs.map +1 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs +66 -0
- package/dist/cjs/components/MediaStatusIndicator/MediaStatusIndicator.cjs.map +1 -0
- package/dist/cjs/components/Select/Select.cjs +61 -17
- package/dist/cjs/components/Select/Select.cjs.map +1 -1
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs +53 -0
- package/dist/cjs/components/SubscribeButton/SubscribeButton.cjs.map +1 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs +305 -0
- package/dist/cjs/components/VoiceNote/VoiceNote.cjs.map +1 -0
- package/dist/cjs/index.cjs +12 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/utils/audioWaveform.cjs +76 -0
- package/dist/cjs/utils/audioWaveform.cjs.map +1 -0
- package/dist/cjs/utils/useAudioPlayback.cjs +123 -0
- package/dist/cjs/utils/useAudioPlayback.cjs.map +1 -0
- package/dist/cjs/utils/useFittedBarCount.cjs +42 -0
- package/dist/cjs/utils/useFittedBarCount.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs +48 -0
- package/dist/cjs/utils/useWaveformPeaks.cjs.map +1 -0
- package/dist/cjs/utils/useWaveformSeek.cjs +100 -0
- package/dist/cjs/utils/useWaveformSeek.cjs.map +1 -0
- package/dist/components/AudioPlayer/AudioPlayer.mjs +7 -80
- package/dist/components/AudioPlayer/AudioPlayer.mjs.map +1 -1
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs +37 -0
- package/dist/components/AudioRecordButton/AudioRecordButton.mjs.map +1 -0
- package/dist/components/AudioUpload/AudioUpload.mjs +5 -5
- package/dist/components/AudioUpload/AudioUpload.mjs.map +1 -1
- package/dist/components/ChatInput/ChatInput.mjs +6 -11
- package/dist/components/ChatInput/ChatInput.mjs.map +1 -1
- package/dist/components/ChatMessage/ChatMessage.mjs +211 -0
- package/dist/components/ChatMessage/ChatMessage.mjs.map +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.mjs +161 -72
- package/dist/components/DropdownMenu/DropdownMenu.mjs.map +1 -1
- package/dist/components/Icons/DenseGridViewIcon.mjs +35 -0
- package/dist/components/Icons/DenseGridViewIcon.mjs.map +1 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs +49 -0
- package/dist/components/MediaStatusIndicator/MediaStatusIndicator.mjs.map +1 -0
- package/dist/components/Select/Select.mjs +61 -17
- package/dist/components/Select/Select.mjs.map +1 -1
- package/dist/components/SubscribeButton/SubscribeButton.mjs +36 -0
- package/dist/components/SubscribeButton/SubscribeButton.mjs.map +1 -0
- package/dist/components/VoiceNote/VoiceNote.mjs +288 -0
- package/dist/components/VoiceNote/VoiceNote.mjs.map +1 -0
- package/dist/index.d.ts +321 -4
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -1
- package/dist/styles/base.css +11 -2
- package/dist/styles/theme.css +1 -1
- package/dist/utils/audioWaveform.mjs +76 -0
- package/dist/utils/audioWaveform.mjs.map +1 -0
- package/dist/utils/useAudioPlayback.mjs +106 -0
- package/dist/utils/useAudioPlayback.mjs.map +1 -0
- package/dist/utils/useFittedBarCount.mjs +25 -0
- package/dist/utils/useFittedBarCount.mjs.map +1 -0
- package/dist/utils/useWaveformPeaks.mjs +31 -0
- package/dist/utils/useWaveformPeaks.mjs.map +1 -0
- package/dist/utils/useWaveformSeek.mjs +83 -0
- package/dist/utils/useWaveformSeek.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DropdownMenu.mjs","sources":["../../../src/components/DropdownMenu/DropdownMenu.tsx"],"sourcesContent":["import * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"../../utils/floatingContentCollisionPadding\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { SearchIcon } from \"../Icons/SearchIcon\";\n\n// Movement, in CSS px, above which a touch press-and-release counts as a drag.\nconst TAP_MOVEMENT_THRESHOLD_PX = 10;\n\n// Keys that the menu must keep handling even when focus is inside a child\n// input — arrows move the highlight into the list, Tab leaves the menu, and\n// Enter / Escape close it.\nconst NAVIGATION_KEYS = new Set([\n \"ArrowDown\",\n \"ArrowUp\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Escape\",\n \"Tab\",\n \"Enter\",\n]);\n\ntype ActiveTap = {\n pointerId: number;\n x: number;\n y: number;\n movedPastThreshold: boolean;\n};\n\n// Lets DropdownMenuTrigger toggle the menu directly so it can gate on touch\n// movement — see radix-ui/primitives#1912.\nconst ToggleOpenContext = React.createContext<\n ((updater: (prev: boolean) => boolean) => void) | null\n>(null);\n\n/** Props for the {@link DropdownMenu} root component. */\nexport interface DropdownMenuProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Root> {}\n\n/** Root component that manages open/close state for a dropdown menu. */\nexport function DropdownMenu({\n open: openProp,\n defaultOpen,\n onOpenChange,\n children,\n ...props\n}: DropdownMenuProps) {\n const [open = false, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n onChange: onOpenChange,\n });\n\n return (\n <ToggleOpenContext.Provider value={setOpen}>\n <DropdownMenuPrimitive.Root open={open} onOpenChange={setOpen} {...props}>\n {children}\n </DropdownMenuPrimitive.Root>\n </ToggleOpenContext.Provider>\n );\n}\n\n/** Props for the {@link DropdownMenuTrigger} component. */\nexport type DropdownMenuTriggerProps = React.ComponentPropsWithoutRef<\n typeof DropdownMenuPrimitive.Trigger\n>;\n\n/**\n * The element that toggles the dropdown menu when clicked.\n *\n * On touch devices, the menu only opens if the press-and-release stays within\n * a small movement threshold. A drag that incidentally ends over the trigger\n * (common when scrolling a feed on Android Chrome) is ignored. Mouse and\n * keyboard interactions are unchanged.\n */\nexport const DropdownMenuTrigger = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Trigger>,\n DropdownMenuTriggerProps\n>((props, ref) => {\n const toggleOpen = React.useContext(ToggleOpenContext);\n const tapRef = React.useRef<ActiveTap | null>(null);\n\n // Used outside our DropdownMenu wrapper — fall through to Radix defaults.\n if (toggleOpen === null) {\n return <DropdownMenuPrimitive.Trigger {...props} ref={ref} />;\n }\n\n return (\n <DropdownMenuPrimitive.Trigger\n {...props}\n ref={ref}\n onPointerDown={(event) => {\n props.onPointerDown?.(event);\n if (event.pointerType === \"mouse\" || props.disabled) return;\n // Keep pointerup / pointercancel on this element if the finger drifts off.\n // Optional because jsdom (used in tests) doesn't implement it.\n event.currentTarget.setPointerCapture?.(event.pointerId);\n tapRef.current = {\n pointerId: event.pointerId,\n x: event.clientX,\n y: event.clientY,\n movedPastThreshold: false,\n };\n // preventDefault stops Radix's pointerdown open path via composeEventHandlers.\n event.preventDefault();\n }}\n onPointerMove={(event) => {\n props.onPointerMove?.(event);\n const tap = tapRef.current;\n if (tap === null || event.pointerId !== tap.pointerId || tap.movedPastThreshold) {\n return;\n }\n const dx = event.clientX - tap.x;\n const dy = event.clientY - tap.y;\n if (Math.hypot(dx, dy) > TAP_MOVEMENT_THRESHOLD_PX) {\n tap.movedPastThreshold = true;\n }\n }}\n onPointerUp={(event) => {\n props.onPointerUp?.(event);\n const tap = tapRef.current;\n if (tap === null || event.pointerId !== tap.pointerId) return;\n const wasDrag = tap.movedPastThreshold;\n tapRef.current = null;\n if (!wasDrag && !props.disabled) {\n toggleOpen((prev) => !prev);\n }\n }}\n onPointerCancel={(event) => {\n props.onPointerCancel?.(event);\n const tap = tapRef.current;\n if (tap !== null && event.pointerId === tap.pointerId) {\n tapRef.current = null;\n }\n }}\n />\n );\n});\nDropdownMenuTrigger.displayName = \"DropdownMenuTrigger\";\n\n/** Props for the {@link DropdownMenuContent} component. */\nexport interface DropdownMenuContentProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> {}\n\n/**\n * The positioned content panel rendered inside a portal.\n *\n * Override the portal z-index per-instance via `style={{ zIndex: 1500 }}` or\n * globally with the `--fanvue-ui-portal-z-index` CSS custom property.\n *\n * @example\n * ```tsx\n * <DropdownMenu>\n * <DropdownMenuTrigger asChild>\n * <Button>Open</Button>\n * </DropdownMenuTrigger>\n * <DropdownMenuContent>\n * <DropdownMenuItem>Option 1</DropdownMenuItem>\n * <DropdownMenuItem>Option 2</DropdownMenuItem>\n * </DropdownMenuContent>\n * </DropdownMenu>\n * ```\n */\nexport const DropdownMenuContent = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Content>,\n DropdownMenuContentProps\n>(\n (\n {\n className,\n style,\n sideOffset = 4,\n collisionPadding = FLOATING_CONTENT_COLLISION_PADDING,\n ...props\n },\n ref,\n ) => (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n collisionPadding={collisionPadding}\n className={cn(\n \"w-max min-w-(--radix-dropdown-menu-trigger-width) max-w-(--radix-dropdown-menu-content-available-width) overflow-y-auto rounded-sm border border-neutral-alphas-200 bg-surface-primary p-1 text-content-primary shadow-lg\",\n \"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n \"data-[side=top]:slide-in-from-bottom-2 data-[side=bottom]:slide-in-from-top-2\",\n \"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2\",\n className,\n )}\n style={{\n zIndex: \"var(--fanvue-ui-portal-z-index, 50)\",\n maxHeight: \"var(--radix-dropdown-menu-content-available-height)\",\n ...style,\n }}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n ),\n);\nDropdownMenuContent.displayName = \"DropdownMenuContent\";\n\n/** Props for the {@link DropdownMenuGroup} component. */\nexport type DropdownMenuGroupProps = React.ComponentPropsWithoutRef<\n typeof DropdownMenuPrimitive.Group\n>;\n\n/** Groups related menu items. Accepts an optional `DropdownMenuLabel`. */\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nDropdownMenuGroup.displayName = \"DropdownMenuGroup\";\n\n/** Vertical placement of a {@link DropdownMenuLabel} within its group. */\nexport type DropdownMenuLabelPosition = \"default\" | \"top\";\n\n/** Props for the {@link DropdownMenuLabel} component. */\nexport interface DropdownMenuLabelProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> {\n /**\n * Vertical placement within the surrounding group. `\"top\"` is used for the\n * first label directly under a header; `\"default\"` adds extra top padding to\n * separate it from preceding items. @default \"default\"\n */\n position?: DropdownMenuLabelPosition;\n}\n\n/** A non-interactive label that groups related items within a menu. */\nexport const DropdownMenuLabel = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Label>,\n DropdownMenuLabelProps\n>(({ className, position = \"default\", ...props }, ref) => (\n <DropdownMenuPrimitive.Label\n ref={ref}\n className={cn(\n \"typography-description-12px-regular flex items-center px-3 text-content-secondary\",\n position === \"top\" ? \"py-2\" : \"pb-2 pt-4\",\n className,\n )}\n {...props}\n />\n));\nDropdownMenuLabel.displayName = \"DropdownMenuLabel\";\n\n/**\n * Height preset for a dropdown menu item.\n *\n * `\"40\"` (default) and `\"32\"` are the v2 numeric tokens that mirror the Figma\n * design system. `\"sm\"` and `\"md\"` are deprecated aliases retained for\n * backwards compatibility — `\"sm\"` maps to `\"32\"`, `\"md\"` maps to `\"40\"`.\n */\nexport type DropdownMenuItemSize =\n | \"40\"\n | \"32\"\n /** @deprecated Use `\"32\"` instead. */\n | \"sm\"\n /** @deprecated Use `\"40\"` instead. */\n | \"md\";\n\nconst SIZE_NORMALIZED: Record<DropdownMenuItemSize, \"40\" | \"32\"> = {\n \"40\": \"40\",\n md: \"40\",\n \"32\": \"32\",\n sm: \"32\",\n};\n\nconst ITEM_SIZE_CLASSES: Record<\"40\" | \"32\", string> = {\n \"40\": \"min-h-10 py-2 typography-body-default-16px-regular\",\n \"32\": \"min-h-8 py-[7px] typography-body-small-14px-regular\",\n};\n\nconst ITEM_SELECTED_TYPOGRAPHY: Record<\"40\" | \"32\", string> = {\n \"40\": \"typography-body-default-16px-semibold\",\n \"32\": \"typography-body-small-14px-semibold\",\n};\n\nconst ITEM_COUNT_TYPOGRAPHY: Record<\"40\" | \"32\", string> = {\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\nexport interface DropdownMenuItemProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> {\n /** Height of the menu item row. @default \"40\" */\n size?: DropdownMenuItemSize;\n /** Applies the destructive (error) treatment. Use for irreversible actions. @default false */\n destructive?: boolean;\n /** Icon (or other node) rendered before the label. Ignored when {@link DropdownMenuItemProps.avatar} is set. */\n leadingIcon?: React.ReactNode;\n /**\n * Leading avatar rendered in place of {@link DropdownMenuItemProps.leadingIcon},\n * for rows that represent a person or account. Pass an `Avatar` sized to `24`.\n * Takes precedence over `leadingIcon`.\n */\n avatar?: React.ReactNode;\n /** Icon (or other node) rendered after the label. */\n trailingIcon?: React.ReactNode;\n /** Trailing count or number (e.g. an unread total) rendered before {@link DropdownMenuItemProps.trailingIcon}. */\n count?: React.ReactNode;\n /**\n * Optional secondary text rendered on a second line below the label. When\n * provided, the row switches to a two-line layout and the leading/trailing\n * icons align to the title line (top) rather than the row's vertical centre.\n */\n description?: React.ReactNode;\n /** Marks the item as the current selection in a single-select menu. @default false */\n selected?: boolean;\n}\n\n/**\n * An individual item within a {@link DropdownMenuContent}.\n *\n * @example\n * ```tsx\n * <DropdownMenuItem>Edit profile</DropdownMenuItem>\n * <DropdownMenuItem destructive>Delete</DropdownMenuItem>\n * <DropdownMenuItem leadingIcon={<EditIcon />}>Edit</DropdownMenuItem>\n *\n * // Feature-rich row with an avatar and a trailing count\n * <DropdownMenuItem avatar={<Avatar size={24} src={src} />} count=\"12\">\n * Jane Doe\n * </DropdownMenuItem>\n *\n * // As a link\n * <DropdownMenuItem asChild>\n * <a href=\"/settings\">Settings</a>\n * </DropdownMenuItem>\n * ```\n */\nexport const DropdownMenuItem = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Item>,\n DropdownMenuItemProps\n>(\n (\n {\n size = \"40\",\n destructive,\n leadingIcon,\n avatar,\n trailingIcon,\n count,\n description,\n selected,\n className,\n children,\n asChild,\n ...props\n },\n ref,\n ) => {\n const normalizedSize = SIZE_NORMALIZED[size];\n const hasDescription = description != null;\n const hasAvatar = avatar != null;\n const itemClassName = cn(\n \"group flex w-full cursor-pointer gap-2 rounded-xs px-3 outline-none\",\n hasDescription ? \"items-start\" : \"items-center\",\n ITEM_SIZE_CLASSES[normalizedSize],\n // A 24px avatar would push the compact 32px row past its height with the\n // default padding; tighten it so the avatar variant keeps the 32px contract.\n hasAvatar && !hasDescription && normalizedSize === \"32\" && \"py-1\",\n \"data-[highlighted]:bg-neutral-alphas-50\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:text-content-disabled\",\n destructive && \"text-error-content\",\n selected && [\n \"bg-buttons-primary-default text-content-primary-inverted\",\n \"data-[highlighted]:bg-buttons-primary-default\",\n ITEM_SELECTED_TYPOGRAPHY[normalizedSize],\n ],\n className,\n );\n\n if (asChild) {\n return (\n <DropdownMenuPrimitive.Item ref={ref} asChild className={itemClassName} {...props}>\n {children}\n </DropdownMenuPrimitive.Item>\n );\n }\n\n // In the two-line (description) layout, icons sit on the title's line.\n // 24px title line-height vs 16px icon → 4px (pt-1) centres the icon on it.\n const iconAlignClassName = hasDescription ? \"flex shrink-0 items-center pt-1\" : null;\n\n const countNode = count != null && (\n <span\n className={cn(\n \"shrink-0 tabular-nums\",\n ITEM_COUNT_TYPOGRAPHY[normalizedSize],\n destructive\n ? \"text-error-content\"\n : selected\n ? \"text-content-primary-inverted\"\n : \"text-content-tertiary\",\n \"group-data-[disabled]:text-content-disabled\",\n )}\n >\n {count}\n </span>\n );\n\n return (\n <DropdownMenuPrimitive.Item ref={ref} className={itemClassName} {...props}>\n {avatar != null ? (\n <span className=\"shrink-0\">{avatar}</span>\n ) : (\n leadingIcon != null &&\n (hasDescription ? (\n <span className={iconAlignClassName!}>{leadingIcon}</span>\n ) : (\n leadingIcon\n ))\n )}\n {hasDescription ? (\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span className=\"truncate\">{children}</span>\n <span\n className={cn(\n \"typography-body-small-14px-regular truncate\",\n selected ? \"text-content-primary-inverted\" : \"text-content-secondary\",\n )}\n >\n {description}\n </span>\n </span>\n ) : (\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n )}\n {countNode}\n {trailingIcon != null &&\n (hasDescription ? (\n <span className={iconAlignClassName!}>{trailingIcon}</span>\n ) : (\n trailingIcon\n ))}\n </DropdownMenuPrimitive.Item>\n );\n },\n);\nDropdownMenuItem.displayName = \"DropdownMenuItem\";\n\n/** Props for the {@link DropdownMenuSeparator} component. */\nexport interface DropdownMenuSeparatorProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> {}\n\n/** Visual separator between groups of items. */\nexport const DropdownMenuSeparator = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,\n DropdownMenuSeparatorProps\n>(({ className, ...props }, ref) => (\n <DropdownMenuPrimitive.Separator\n ref={ref}\n className={cn(\"my-1 h-px bg-neutral-alphas-200\", className)}\n {...props}\n />\n));\nDropdownMenuSeparator.displayName = \"DropdownMenuSeparator\";\n\n/** Header type. `\"default\"` shows a title; `\"search\"` shows a search input. */\nexport type DropdownMenuHeaderType = \"default\" | \"search\";\n\n/** Header height preset. Matches the menu item sizing scale. */\nexport type DropdownMenuHeaderSize = \"40\" | \"32\";\n\n/** Search-input configuration for {@link DropdownMenuHeader} when `type=\"search\"`. */\nexport interface DropdownMenuHeaderSearchProps {\n /** Controlled value of the search input. */\n value?: string;\n /** Uncontrolled default value. */\n defaultValue?: string;\n /** Fires when the input value changes. */\n onChange?: (value: string) => void;\n /** Placeholder text shown when the input is empty. @default \"Search…\" */\n placeholder?: string;\n /** Accessible label for the search input. @default \"Search\" */\n \"aria-label\"?: string;\n}\n\nexport interface DropdownMenuHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Visual type. `\"default\"` shows a title; `\"search\"` shows a search input. @default \"default\" */\n type?: DropdownMenuHeaderType;\n /** Height preset for the header row. @default \"40\" */\n size?: DropdownMenuHeaderSize;\n /** Title text shown when `type=\"default\"`. Ignored if `children` is provided. */\n title?: string;\n /** Configuration for the embedded search input when `type=\"search\"`. */\n searchProps?: DropdownMenuHeaderSearchProps;\n /** Whether to render the close icon button on the right. @default true */\n showClose?: boolean;\n /** Fires when the close icon button is activated. */\n onClose?: () => void;\n /** Accessible label for the close button. @default \"Close menu\" */\n closeLabel?: string;\n}\n\n/**\n * Optional header rendered at the top of a {@link DropdownMenuContent}. Use\n * `type=\"default\"` to title the menu, or `type=\"search\"` to embed a search\n * input for filtering long lists.\n *\n * Renders an inset separator beneath the row so it slots cleanly above the\n * first group of items.\n *\n * @example\n * ```tsx\n * <DropdownMenuContent>\n * <DropdownMenuHeader title=\"Sort by\" onClose={() => setOpen(false)} />\n * <DropdownMenuItem>Newest</DropdownMenuItem>\n * <DropdownMenuItem>Oldest</DropdownMenuItem>\n * </DropdownMenuContent>\n * ```\n */\nexport const DropdownMenuHeader = React.forwardRef<HTMLDivElement, DropdownMenuHeaderProps>(\n (\n {\n type = \"default\",\n size = \"40\",\n title,\n searchProps,\n showClose = true,\n onClose,\n closeLabel = \"Close menu\",\n className,\n children,\n ...props\n },\n ref,\n ) => {\n const titleTypography =\n size === \"32\"\n ? \"typography-body-small-14px-semibold\"\n : \"typography-body-default-16px-semibold\";\n const toggleOpen = React.useContext(ToggleOpenContext);\n\n const handleClose = () => {\n onClose?.();\n // Also dismiss the Radix menu when used in uncontrolled mode — otherwise\n // the close button would look broken to consumers that don't wire up\n // `open` / `onOpenChange` themselves.\n toggleOpen?.(() => false);\n };\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex flex-col px-1 pt-1 mb-1\",\n // Search needs an 8px gap between the input and the divider; the\n // default (title) variant uses 4px because the title baseline sits\n // closer to the divider naturally.\n type === \"search\" ? \"gap-2\" : \"gap-1\",\n className,\n )}\n {...props}\n >\n <div className=\"flex items-center gap-4 pl-2\">\n {type === \"default\" ? (\n <div className={cn(\"min-w-0 flex-1 truncate text-content-primary\", titleTypography)}>\n {children ?? title}\n </div>\n ) : (\n <SearchInput {...searchProps} />\n )}\n {showClose && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<CloseIcon />}\n onClick={handleClose}\n aria-label={closeLabel}\n />\n )}\n </div>\n <DropdownMenuSeparator className=\"my-0\" />\n </div>\n );\n },\n);\nDropdownMenuHeader.displayName = \"DropdownMenuHeader\";\n\nfunction SearchInput({\n value,\n defaultValue,\n onChange,\n placeholder = \"Search\\u2026\",\n \"aria-label\": ariaLabel = \"Search\",\n}: DropdownMenuHeaderSearchProps = {}) {\n return (\n <label\n className={cn(\n \"flex min-w-0 flex-1 items-center gap-2 rounded-xs border border-border-primary\",\n \"bg-neutral-alphas-50 px-3 py-1 text-content-primary\",\n \"focus-within:shadow-focus-ring focus-within:outline-none\",\n )}\n >\n <SearchIcon className=\"size-4 shrink-0 text-content-tertiary\" aria-hidden=\"true\" />\n <input\n type=\"search\"\n className={cn(\n \"typography-body-default-16px-regular min-w-0 flex-1 bg-transparent outline-none\",\n \"placeholder:text-content-tertiary\",\n )}\n value={value}\n defaultValue={defaultValue}\n placeholder={placeholder}\n aria-label={ariaLabel}\n onChange={(event) => onChange?.(event.target.value)}\n // Radix DropdownMenu listens for keystrokes on Content for its\n // typeahead (typing letters jumps focus to a matching item). That\n // listener steals focus from the input after the first letter, so we\n // stop character keys from bubbling. Navigation keys (arrows / Tab /\n // Escape / Enter) are still allowed through so the user can leave the\n // input for the list or close the menu. Pointer events are stopped so\n // clicking back into the input doesn't fight the menu's focus\n // management either.\n onKeyDown={(event) => {\n if (!NAVIGATION_KEYS.has(event.key)) event.stopPropagation();\n }}\n onPointerDown={(event) => event.stopPropagation()}\n onMouseDown={(event) => event.stopPropagation()}\n />\n </label>\n );\n}\n\n/** Props for the {@link DropdownMenuRadioGroup} component. */\nexport interface DropdownMenuRadioGroupProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioGroup> {}\n\n/**\n * Groups {@link DropdownMenuRadioItem} children so they behave as a\n * single-select set. Controlled via `value`/`onValueChange`.\n *\n * @example\n * ```tsx\n * <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>\n * <DropdownMenuRadioItem value=\"newest\">Newest first</DropdownMenuRadioItem>\n * <DropdownMenuRadioItem value=\"oldest\">Oldest first</DropdownMenuRadioItem>\n * </DropdownMenuRadioGroup>\n * ```\n */\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\n/** Height preset for a {@link DropdownMenuRadioItem}. */\nexport type DropdownMenuRadioItemSize = \"40\";\n\nexport interface DropdownMenuRadioItemProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> {\n /** Optional secondary text shown below the title. */\n helper?: string;\n /** Height of the item row. @default \"40\" */\n size?: DropdownMenuRadioItemSize;\n}\n\n/**\n * A single radio-style choice within a {@link DropdownMenuRadioGroup}. Shows\n * a circular indicator that fills when selected, plus an optional helper line\n * underneath the title.\n */\nexport const DropdownMenuRadioItem = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,\n DropdownMenuRadioItemProps\n>(({ className, children, helper, size: _size = \"40\", ...props }, ref) => {\n return (\n <DropdownMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n \"group flex w-full cursor-pointer items-start gap-3 rounded-xs px-4 py-2 outline-none\",\n \"data-[highlighted]:bg-neutral-alphas-50\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:text-content-disabled\",\n \"data-[state=checked]:bg-buttons-primary-default data-[state=checked]:text-content-primary-inverted\",\n \"data-[state=checked]:data-[highlighted]:bg-buttons-primary-default\",\n className,\n )}\n {...props}\n >\n <span\n className={cn(\n \"mt-1 flex size-4 shrink-0 items-center justify-center rounded-full border border-icons-primary\",\n \"group-data-[disabled]:border-content-disabled\",\n \"group-data-[state=checked]:border-icons-primary-inverted\",\n )}\n aria-hidden=\"true\"\n >\n <DropdownMenuPrimitive.ItemIndicator asChild>\n <span className=\"size-2 rounded-full bg-content-primary-inverted\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span className=\"typography-body-default-16px-semibold truncate\">{children}</span>\n {helper && (\n <span\n className={cn(\n \"typography-description-12px-regular text-content-secondary\",\n \"group-data-[state=checked]:text-content-primary-inverted\",\n \"group-data-[disabled]:text-content-disabled\",\n )}\n >\n {helper}\n </span>\n )}\n </span>\n </DropdownMenuPrimitive.RadioItem>\n );\n});\nDropdownMenuRadioItem.displayName = \"DropdownMenuRadioItem\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DropdownMenu.mjs","sources":["../../../src/components/DropdownMenu/DropdownMenu.tsx"],"sourcesContent":["import * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"../../utils/floatingContentCollisionPadding\";\nimport { Drawer, DrawerContent, DrawerTrigger } from \"../Drawer/Drawer\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CheckIcon } from \"../Icons/CheckIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { SearchIcon } from \"../Icons/SearchIcon\";\n\n// Movement, in CSS px, above which a touch press-and-release counts as a drag.\nconst TAP_MOVEMENT_THRESHOLD_PX = 10;\n\n// Keys that the menu must keep handling even when focus is inside a child\n// input — arrows move the highlight into the list, Tab leaves the menu, and\n// Enter / Escape close it.\nconst NAVIGATION_KEYS = new Set([\n \"ArrowDown\",\n \"ArrowUp\",\n \"ArrowLeft\",\n \"ArrowRight\",\n \"Escape\",\n \"Tab\",\n \"Enter\",\n]);\n\ntype ActiveTap = {\n pointerId: number;\n x: number;\n y: number;\n movedPastThreshold: boolean;\n};\n\n// Lets DropdownMenuTrigger toggle the menu directly so it can gate on touch\n// movement — see radix-ui/primitives#1912.\nconst ToggleOpenContext = React.createContext<\n ((updater: (prev: boolean) => boolean) => void) | null\n>(null);\n\n/**\n * How a {@link DropdownMenu} presents its content.\n * - `\"menu\"` (default) — a Radix-positioned panel anchored to the trigger.\n * - `\"sheet\"` — a bottom drawer (via {@link Drawer}), for mobile/touch viewports.\n *\n * The viewport decision belongs to the consumer (it owns the breakpoint\n * source of truth), so pass e.g. `variant={isDesktop ? \"menu\" : \"sheet\"}`.\n */\nexport type DropdownMenuVariant = \"menu\" | \"sheet\";\n\nconst DropdownMenuVariantContext = React.createContext<DropdownMenuVariant>(\"menu\");\n\n/** Props for the {@link DropdownMenu} root component. */\nexport interface DropdownMenuProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Root> {\n /** How the menu presents its content. @default \"menu\" */\n variant?: DropdownMenuVariant;\n}\n\n/** Root component that manages open/close state for a dropdown menu. */\nexport function DropdownMenu({\n open: openProp,\n defaultOpen,\n onOpenChange,\n variant = \"menu\",\n children,\n ...props\n}: DropdownMenuProps) {\n const [open = false, setOpen] = useControllableState({\n prop: openProp,\n defaultProp: defaultOpen ?? false,\n onChange: onOpenChange,\n });\n\n if (variant === \"sheet\") {\n return (\n <DropdownMenuVariantContext.Provider value=\"sheet\">\n <ToggleOpenContext.Provider value={setOpen}>\n <Drawer open={open} onOpenChange={setOpen}>\n {children}\n </Drawer>\n </ToggleOpenContext.Provider>\n </DropdownMenuVariantContext.Provider>\n );\n }\n\n return (\n <DropdownMenuVariantContext.Provider value=\"menu\">\n <ToggleOpenContext.Provider value={setOpen}>\n <DropdownMenuPrimitive.Root open={open} onOpenChange={setOpen} {...props}>\n {children}\n </DropdownMenuPrimitive.Root>\n </ToggleOpenContext.Provider>\n </DropdownMenuVariantContext.Provider>\n );\n}\n\n/** Props for the {@link DropdownMenuTrigger} component. */\nexport type DropdownMenuTriggerProps = React.ComponentPropsWithoutRef<\n typeof DropdownMenuPrimitive.Trigger\n>;\n\n/**\n * The element that toggles the dropdown menu when clicked.\n *\n * On touch devices, the menu only opens if the press-and-release stays within\n * a small movement threshold. A drag that incidentally ends over the trigger\n * (common when scrolling a feed on Android Chrome) is ignored. Mouse and\n * keyboard interactions are unchanged.\n */\nexport const DropdownMenuTrigger = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Trigger>,\n DropdownMenuTriggerProps\n>((props, ref) => {\n const variant = React.useContext(DropdownMenuVariantContext);\n const toggleOpen = React.useContext(ToggleOpenContext);\n const tapRef = React.useRef<ActiveTap | null>(null);\n\n // The sheet variant opens a Drawer (Dialog), which already suppresses\n // scroll-drag-end synthetic clicks itself — no need for the touch-tap gating below.\n if (variant === \"sheet\") {\n return <DrawerTrigger {...props} ref={ref} />;\n }\n\n // Used outside our DropdownMenu wrapper — fall through to Radix defaults.\n if (toggleOpen === null) {\n return <DropdownMenuPrimitive.Trigger {...props} ref={ref} />;\n }\n\n return (\n <DropdownMenuPrimitive.Trigger\n {...props}\n ref={ref}\n onPointerDown={(event) => {\n props.onPointerDown?.(event);\n if (event.pointerType === \"mouse\" || props.disabled) return;\n // Keep pointerup / pointercancel on this element if the finger drifts off.\n // Optional because jsdom (used in tests) doesn't implement it.\n event.currentTarget.setPointerCapture?.(event.pointerId);\n tapRef.current = {\n pointerId: event.pointerId,\n x: event.clientX,\n y: event.clientY,\n movedPastThreshold: false,\n };\n // preventDefault stops Radix's pointerdown open path via composeEventHandlers.\n event.preventDefault();\n }}\n onPointerMove={(event) => {\n props.onPointerMove?.(event);\n const tap = tapRef.current;\n if (tap === null || event.pointerId !== tap.pointerId || tap.movedPastThreshold) {\n return;\n }\n const dx = event.clientX - tap.x;\n const dy = event.clientY - tap.y;\n if (Math.hypot(dx, dy) > TAP_MOVEMENT_THRESHOLD_PX) {\n tap.movedPastThreshold = true;\n }\n }}\n onPointerUp={(event) => {\n props.onPointerUp?.(event);\n const tap = tapRef.current;\n if (tap === null || event.pointerId !== tap.pointerId) return;\n const wasDrag = tap.movedPastThreshold;\n tapRef.current = null;\n if (!wasDrag && !props.disabled) {\n toggleOpen((prev) => !prev);\n }\n }}\n onPointerCancel={(event) => {\n props.onPointerCancel?.(event);\n const tap = tapRef.current;\n if (tap !== null && event.pointerId === tap.pointerId) {\n tapRef.current = null;\n }\n }}\n />\n );\n});\nDropdownMenuTrigger.displayName = \"DropdownMenuTrigger\";\n\n/** Props for the {@link DropdownMenuContent} component. */\nexport interface DropdownMenuContentProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> {}\n\n/**\n * The positioned content panel rendered inside a portal.\n *\n * Override the portal z-index per-instance via `style={{ zIndex: 1500 }}` or\n * globally with the `--fanvue-ui-portal-z-index` CSS custom property.\n *\n * @example\n * ```tsx\n * <DropdownMenu>\n * <DropdownMenuTrigger asChild>\n * <Button>Open</Button>\n * </DropdownMenuTrigger>\n * <DropdownMenuContent>\n * <DropdownMenuItem>Option 1</DropdownMenuItem>\n * <DropdownMenuItem>Option 2</DropdownMenuItem>\n * </DropdownMenuContent>\n * </DropdownMenu>\n * ```\n */\nexport const DropdownMenuContent = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Content>,\n DropdownMenuContentProps\n>(\n (\n {\n className,\n style,\n sideOffset = 4,\n // Radix defaults `avoidCollisions` to true, so passing `collisionPadding`\n // is enough to keep the menu flipping/shifting to stay on screen — no\n // hand-rolled reposition logic needed.\n collisionPadding = FLOATING_CONTENT_COLLISION_PADDING,\n children,\n ...props\n },\n ref,\n ) => {\n const variant = React.useContext(DropdownMenuVariantContext);\n\n if (variant === \"sheet\") {\n return (\n <DrawerContent\n ref={ref}\n position=\"bottom\"\n variant=\"sheet\"\n className={cn(\"flex flex-col gap-1 p-1\", className)}\n style={style}\n {...props}\n >\n {children}\n </DrawerContent>\n );\n }\n\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n ref={ref}\n sideOffset={sideOffset}\n collisionPadding={collisionPadding}\n className={cn(\n \"w-max min-w-(--radix-dropdown-menu-trigger-width) max-w-(--radix-dropdown-menu-content-available-width) overflow-y-auto rounded-sm border border-neutral-alphas-200 bg-surface-primary p-1 text-content-primary shadow-lg\",\n \"data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\",\n \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\",\n \"data-[side=top]:slide-in-from-bottom-2 data-[side=bottom]:slide-in-from-top-2\",\n \"data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2\",\n className,\n )}\n style={{\n zIndex: \"var(--fanvue-ui-portal-z-index, 50)\",\n maxHeight: \"var(--radix-dropdown-menu-content-available-height)\",\n ...style,\n }}\n {...props}\n >\n {children}\n </DropdownMenuPrimitive.Content>\n </DropdownMenuPrimitive.Portal>\n );\n },\n);\nDropdownMenuContent.displayName = \"DropdownMenuContent\";\n\n/** Props for the {@link DropdownMenuGroup} component. */\nexport type DropdownMenuGroupProps = React.ComponentPropsWithoutRef<\n typeof DropdownMenuPrimitive.Group\n>;\n\n/**\n * Groups related menu items. Accepts an optional `DropdownMenuLabel`.\n *\n * Requires Radix menu context — not supported inside a `variant=\"sheet\"` menu.\n */\nexport const DropdownMenuGroup = DropdownMenuPrimitive.Group;\nDropdownMenuGroup.displayName = \"DropdownMenuGroup\";\n\n/** Vertical placement of a {@link DropdownMenuLabel} within its group. */\nexport type DropdownMenuLabelPosition = \"default\" | \"top\";\n\n/** Props for the {@link DropdownMenuLabel} component. */\nexport interface DropdownMenuLabelProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> {\n /**\n * Vertical placement within the surrounding group. `\"top\"` is used for the\n * first label directly under a header; `\"default\"` adds extra top padding to\n * separate it from preceding items. @default \"default\"\n */\n position?: DropdownMenuLabelPosition;\n}\n\n/** A non-interactive label that groups related items within a menu. */\nexport const DropdownMenuLabel = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Label>,\n DropdownMenuLabelProps\n>(({ className, position = \"default\", ...props }, ref) => {\n const variant = React.useContext(DropdownMenuVariantContext);\n const labelClassName = cn(\n \"typography-description-12px-regular flex items-center px-3 text-content-secondary\",\n position === \"top\" ? \"py-2\" : \"pb-2 pt-4\",\n className,\n );\n\n // DropdownMenuPrimitive.Label requires Radix menu context, unavailable when\n // the sheet variant renders inside a Drawer (Dialog) instead.\n if (variant === \"sheet\") {\n return <div ref={ref} className={labelClassName} {...props} />;\n }\n\n return <DropdownMenuPrimitive.Label ref={ref} className={labelClassName} {...props} />;\n});\nDropdownMenuLabel.displayName = \"DropdownMenuLabel\";\n\n/**\n * Height preset for a dropdown menu item.\n *\n * `\"40\"` (default) and `\"32\"` are the v2 numeric tokens that mirror the Figma\n * design system. `\"sm\"` and `\"md\"` are deprecated aliases retained for\n * backwards compatibility — `\"sm\"` maps to `\"32\"`, `\"md\"` maps to `\"40\"`.\n */\nexport type DropdownMenuItemSize =\n | \"40\"\n | \"32\"\n /** @deprecated Use `\"32\"` instead. */\n | \"sm\"\n /** @deprecated Use `\"40\"` instead. */\n | \"md\";\n\nconst SIZE_NORMALIZED: Record<DropdownMenuItemSize, \"40\" | \"32\"> = {\n \"40\": \"40\",\n md: \"40\",\n \"32\": \"32\",\n sm: \"32\",\n};\n\nconst ITEM_SIZE_CLASSES: Record<\"40\" | \"32\", string> = {\n \"40\": \"min-h-10 py-2 typography-body-default-16px-regular\",\n \"32\": \"min-h-8 py-[7px] typography-body-small-14px-regular\",\n};\n\nconst ITEM_COUNT_TYPOGRAPHY: Record<\"40\" | \"32\", string> = {\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\n// Background alone can't reliably tell \"selected\" apart from a\n// hovered-but-unselected row across every theme/contrast combination (see the\n// neutral-alphas fix on itemClassName below) — pair it with an explicit\n// indicator, matching SelectItem's check indicator for the same V2 Menu Item\n// spec.\nfunction SelectedCheckIndicator({ hasDescription }: { hasDescription: boolean }) {\n return (\n <CheckIcon\n className={cn(\"size-4 shrink-0 text-content-primary\", hasDescription && \"self-start\")}\n />\n );\n}\n\nexport interface DropdownMenuItemProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> {\n /** Height of the menu item row. @default \"40\" */\n size?: DropdownMenuItemSize;\n /** Applies the destructive (error) treatment. Use for irreversible actions. @default false */\n destructive?: boolean;\n /** Icon (or other node) rendered before the label. Ignored when {@link DropdownMenuItemProps.avatar} is set. */\n leadingIcon?: React.ReactNode;\n /**\n * Leading avatar rendered in place of {@link DropdownMenuItemProps.leadingIcon},\n * for rows that represent a person or account. Pass an `Avatar` sized to `24`.\n * Takes precedence over `leadingIcon`.\n */\n avatar?: React.ReactNode;\n /**\n * Icon (or other node) rendered after the label. When\n * {@link DropdownMenuItemProps.selected} is true and no `trailingIcon` is\n * given, the built-in selected check indicator renders in this slot\n * instead — pass a `trailingIcon` to use a custom selected indicator (e.g.\n * a themed tick) rather than the default one.\n */\n trailingIcon?: React.ReactNode;\n /** Trailing count or number (e.g. an unread total) rendered before {@link DropdownMenuItemProps.trailingIcon}. */\n count?: React.ReactNode;\n /**\n * Optional secondary text rendered on a second line below the label. When\n * provided, the row switches to a two-line layout and the leading/trailing\n * icons align to the title line (top) rather than the row's vertical centre.\n */\n description?: React.ReactNode;\n /** Marks the item as the current selection in a single-select menu. @default false */\n selected?: boolean;\n}\n\n/**\n * An individual item within a {@link DropdownMenuContent}.\n *\n * @example\n * ```tsx\n * <DropdownMenuItem>Edit profile</DropdownMenuItem>\n * <DropdownMenuItem destructive>Delete</DropdownMenuItem>\n * <DropdownMenuItem leadingIcon={<EditIcon />}>Edit</DropdownMenuItem>\n *\n * // Feature-rich row with an avatar and a trailing count\n * <DropdownMenuItem avatar={<Avatar size={24} src={src} />} count=\"12\">\n * Jane Doe\n * </DropdownMenuItem>\n *\n * // As a link\n * <DropdownMenuItem asChild>\n * <a href=\"/settings\">Settings</a>\n * </DropdownMenuItem>\n * ```\n */\nexport const DropdownMenuItem = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Item>,\n DropdownMenuItemProps\n>(\n (\n {\n size = \"40\",\n destructive,\n leadingIcon,\n avatar,\n trailingIcon,\n count,\n description,\n selected,\n className,\n children,\n asChild,\n onSelect,\n disabled,\n ...props\n },\n ref,\n ) => {\n const variant = React.useContext(DropdownMenuVariantContext);\n const toggleOpen = React.useContext(ToggleOpenContext);\n const normalizedSize = SIZE_NORMALIZED[size];\n const hasDescription = description != null;\n const hasAvatar = avatar != null;\n const itemClassName = cn(\n \"group flex w-full cursor-pointer gap-2 rounded-xs px-3 outline-none\",\n hasDescription ? \"items-start\" : \"items-center\",\n ITEM_SIZE_CLASSES[normalizedSize],\n // A 24px avatar would push the compact 32px row past its height with the\n // default padding; tighten it so the avatar variant keeps the 32px contract.\n hasAvatar && !hasDescription && normalizedSize === \"32\" && \"py-1\",\n \"data-[highlighted]:bg-neutral-alphas-50\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:text-content-disabled\",\n \"disabled:cursor-not-allowed disabled:text-content-disabled\",\n // Sheet-variant asChild items are marked disabled via aria-disabled\n // (see below), not the native disabled attribute or Radix's\n // data-disabled — neither selector above matches them.\n \"aria-disabled:cursor-not-allowed aria-disabled:text-content-disabled\",\n destructive && \"text-error-content\",\n // bg-interaction-hover aliases to the same token as the plain hover\n // background above, so a selected row would be indistinguishable from a\n // hovered-but-unselected one. Use the next step up the neutral-alphas\n // ramp instead (still a subtle overlay, not the heavy filled style).\n selected && [\"bg-neutral-alphas-100\", \"data-[highlighted]:bg-neutral-alphas-200\"],\n className,\n );\n\n // In the two-line (description) layout, icons sit on the title's line.\n // 24px title line-height vs 16px icon → 4px (pt-1) centres the icon on it.\n const iconAlignClassName = hasDescription ? \"flex shrink-0 items-center pt-1\" : null;\n\n const countNode = count != null && (\n <span\n className={cn(\n \"shrink-0 tabular-nums\",\n ITEM_COUNT_TYPOGRAPHY[normalizedSize],\n destructive ? \"text-error-content\" : \"text-content-tertiary\",\n \"group-data-[disabled]:text-content-disabled\",\n )}\n >\n {count}\n </span>\n );\n\n // A caller-supplied trailingIcon always wins the trailing slot — some\n // consumers pass their own selected indicator (e.g. a themed tick) and\n // rely on it being shown as-is rather than replaced. Only fall back to\n // the built-in check indicator when selected and no trailingIcon is given.\n const trailingNode =\n trailingIcon != null ? (\n hasDescription ? (\n <span className={iconAlignClassName!}>{trailingIcon}</span>\n ) : (\n trailingIcon\n )\n ) : (\n selected && <SelectedCheckIndicator hasDescription={hasDescription} />\n );\n\n const itemChildren = (\n <>\n {avatar != null ? (\n <span className=\"shrink-0\">{avatar}</span>\n ) : (\n leadingIcon != null &&\n (hasDescription ? (\n <span className={iconAlignClassName!}>{leadingIcon}</span>\n ) : (\n leadingIcon\n ))\n )}\n {hasDescription ? (\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span className=\"truncate\">{children}</span>\n <span className=\"typography-body-small-14px-regular truncate text-content-secondary\">\n {description}\n </span>\n </span>\n ) : (\n <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n )}\n {countNode}\n {trailingNode}\n </>\n );\n\n // The sheet variant renders inside a Drawer (Dialog), not a Radix menu, so\n // DropdownMenuPrimitive.Item (which requires menu context) can't be used —\n // render a plain option element with equivalent selection semantics instead.\n // asChild goes through the same Slot primitive Radix's own Item uses\n // internally, so a custom element (e.g. a link) gets the option\n // role/handlers merged onto it without needing menu context.\n if (variant === \"sheet\") {\n const Comp = asChild ? Slot : \"button\";\n // Pull the consumer's onClick out of the passthrough spread so the\n // handler below can compose with it instead of the spread order\n // silently overwriting it (an explicit onClick after {...props} always\n // wins over the spread's).\n const { onClick: consumerOnClick, ...restProps } =\n props as React.ComponentPropsWithoutRef<\"button\">;\n const sheetSpecificProps = !asChild\n ? { type: \"button\" as const, disabled }\n : disabled\n ? { \"aria-disabled\": true }\n : {};\n return (\n <Comp\n ref={ref as React.Ref<HTMLButtonElement>}\n {...restProps}\n {...sheetSpecificProps}\n role=\"option\"\n aria-selected={selected}\n className={itemClassName}\n onClick={(event: React.MouseEvent<HTMLButtonElement>) => {\n // A native <button disabled> already blocks its click event, but\n // asChild's element (e.g. a link) has no such enforcement — guard\n // explicitly, and prevent the click's default action (e.g. anchor\n // navigation) since aria-disabled alone doesn't stop it.\n if (disabled) {\n event.preventDefault();\n return;\n }\n consumerOnClick?.(event);\n if (event.defaultPrevented) return;\n // Pass the real native event through, not a fake partial shape —\n // handlers that call any Event API beyond preventDefault (e.g.\n // stopPropagation, composedPath) need it to actually exist. It's\n // still live (currentTarget/target are valid) since we're inside\n // the same synchronous click handler that produced it.\n onSelect?.(event.nativeEvent);\n if (!event.nativeEvent.defaultPrevented) toggleOpen?.(() => false);\n }}\n >\n {asChild ? children : itemChildren}\n </Comp>\n );\n }\n\n if (asChild) {\n return (\n <DropdownMenuPrimitive.Item\n ref={ref}\n asChild\n className={itemClassName}\n disabled={disabled}\n onSelect={onSelect}\n {...props}\n >\n {children}\n </DropdownMenuPrimitive.Item>\n );\n }\n\n return (\n <DropdownMenuPrimitive.Item\n ref={ref}\n className={itemClassName}\n disabled={disabled}\n onSelect={onSelect}\n {...props}\n >\n {itemChildren}\n </DropdownMenuPrimitive.Item>\n );\n },\n);\nDropdownMenuItem.displayName = \"DropdownMenuItem\";\n\n/** Props for the {@link DropdownMenuSeparator} component. */\nexport interface DropdownMenuSeparatorProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> {}\n\n/** Visual separator between groups of items. */\nexport const DropdownMenuSeparator = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,\n DropdownMenuSeparatorProps\n>(({ className, ...props }, ref) => {\n const variant = React.useContext(DropdownMenuVariantContext);\n const separatorClassName = cn(\"my-1 h-px bg-neutral-alphas-200\", className);\n\n // DropdownMenuPrimitive.Separator requires Radix menu context, unavailable\n // when the sheet variant renders inside a Drawer (Dialog) instead. <hr> is a\n // native separator, so it needs no ARIA role.\n if (variant === \"sheet\") {\n return <hr ref={ref as React.Ref<HTMLHRElement>} className={separatorClassName} {...props} />;\n }\n\n return <DropdownMenuPrimitive.Separator ref={ref} className={separatorClassName} {...props} />;\n});\nDropdownMenuSeparator.displayName = \"DropdownMenuSeparator\";\n\n/** Header type. `\"default\"` shows a title; `\"search\"` shows a search input. */\nexport type DropdownMenuHeaderType = \"default\" | \"search\";\n\n/** Header height preset. Matches the menu item sizing scale. */\nexport type DropdownMenuHeaderSize = \"40\" | \"32\";\n\n/** Search-input configuration for {@link DropdownMenuHeader} when `type=\"search\"`. */\nexport interface DropdownMenuHeaderSearchProps {\n /** Controlled value of the search input. */\n value?: string;\n /** Uncontrolled default value. */\n defaultValue?: string;\n /** Fires when the input value changes. */\n onChange?: (value: string) => void;\n /** Placeholder text shown when the input is empty. @default \"Search…\" */\n placeholder?: string;\n /** Accessible label for the search input. @default \"Search\" */\n \"aria-label\"?: string;\n}\n\nexport interface DropdownMenuHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /** Visual type. `\"default\"` shows a title; `\"search\"` shows a search input. @default \"default\" */\n type?: DropdownMenuHeaderType;\n /** Height preset for the header row. @default \"40\" */\n size?: DropdownMenuHeaderSize;\n /** Title text shown when `type=\"default\"`. Ignored if `children` is provided. */\n title?: string;\n /** Configuration for the embedded search input when `type=\"search\"`. */\n searchProps?: DropdownMenuHeaderSearchProps;\n /** Whether to render the close icon button on the right. @default true */\n showClose?: boolean;\n /** Fires when the close icon button is activated. */\n onClose?: () => void;\n /** Accessible label for the close button. @default \"Close menu\" */\n closeLabel?: string;\n}\n\n/**\n * Optional header rendered at the top of a {@link DropdownMenuContent}. Use\n * `type=\"default\"` to title the menu, or `type=\"search\"` to embed a search\n * input for filtering long lists.\n *\n * Renders an inset separator beneath the row so it slots cleanly above the\n * first group of items.\n *\n * @example\n * ```tsx\n * <DropdownMenuContent>\n * <DropdownMenuHeader title=\"Sort by\" onClose={() => setOpen(false)} />\n * <DropdownMenuItem>Newest</DropdownMenuItem>\n * <DropdownMenuItem>Oldest</DropdownMenuItem>\n * </DropdownMenuContent>\n * ```\n */\nexport const DropdownMenuHeader = React.forwardRef<HTMLDivElement, DropdownMenuHeaderProps>(\n (\n {\n type = \"default\",\n size = \"40\",\n title,\n searchProps,\n showClose = true,\n onClose,\n closeLabel = \"Close menu\",\n className,\n children,\n ...props\n },\n ref,\n ) => {\n const titleTypography =\n size === \"32\"\n ? \"typography-body-small-14px-semibold\"\n : \"typography-body-default-16px-semibold\";\n const toggleOpen = React.useContext(ToggleOpenContext);\n\n const handleClose = () => {\n onClose?.();\n // Also dismiss the Radix menu when used in uncontrolled mode — otherwise\n // the close button would look broken to consumers that don't wire up\n // `open` / `onOpenChange` themselves.\n toggleOpen?.(() => false);\n };\n\n return (\n <div\n ref={ref}\n className={cn(\n \"flex flex-col px-1 pt-1 mb-1\",\n // Search needs an 8px gap between the input and the divider; the\n // default (title) variant uses 4px because the title baseline sits\n // closer to the divider naturally.\n type === \"search\" ? \"gap-2\" : \"gap-1\",\n className,\n )}\n {...props}\n >\n <div className=\"flex items-center gap-4 pl-2\">\n {type === \"default\" ? (\n <div className={cn(\"min-w-0 flex-1 truncate text-content-primary\", titleTypography)}>\n {children ?? title}\n </div>\n ) : (\n <SearchInput {...searchProps} />\n )}\n {showClose && (\n <IconButton\n variant=\"tertiary\"\n size=\"32\"\n icon={<CloseIcon />}\n onClick={handleClose}\n aria-label={closeLabel}\n />\n )}\n </div>\n <DropdownMenuSeparator className=\"my-0\" />\n </div>\n );\n },\n);\nDropdownMenuHeader.displayName = \"DropdownMenuHeader\";\n\nfunction SearchInput({\n value,\n defaultValue,\n onChange,\n placeholder = \"Search\\u2026\",\n \"aria-label\": ariaLabel = \"Search\",\n}: DropdownMenuHeaderSearchProps = {}) {\n return (\n <label\n className={cn(\n \"flex min-w-0 flex-1 items-center gap-2 rounded-xs border border-border-primary\",\n \"bg-neutral-alphas-50 px-3 py-1 text-content-primary\",\n \"focus-within:shadow-focus-ring focus-within:outline-none\",\n )}\n >\n <SearchIcon className=\"size-4 shrink-0 text-content-tertiary\" aria-hidden=\"true\" />\n <input\n type=\"search\"\n className={cn(\n \"typography-body-default-16px-regular min-w-0 flex-1 bg-transparent outline-none\",\n \"placeholder:text-content-tertiary\",\n )}\n value={value}\n defaultValue={defaultValue}\n placeholder={placeholder}\n aria-label={ariaLabel}\n onChange={(event) => onChange?.(event.target.value)}\n // Radix DropdownMenu listens for keystrokes on Content for its\n // typeahead (typing letters jumps focus to a matching item). That\n // listener steals focus from the input after the first letter, so we\n // stop character keys from bubbling. Navigation keys (arrows / Tab /\n // Escape / Enter) are still allowed through so the user can leave the\n // input for the list or close the menu. Pointer events are stopped so\n // clicking back into the input doesn't fight the menu's focus\n // management either.\n onKeyDown={(event) => {\n if (!NAVIGATION_KEYS.has(event.key)) event.stopPropagation();\n }}\n onPointerDown={(event) => event.stopPropagation()}\n onMouseDown={(event) => event.stopPropagation()}\n />\n </label>\n );\n}\n\n/** Props for the {@link DropdownMenuRadioGroup} component. */\nexport interface DropdownMenuRadioGroupProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioGroup> {}\n\n/**\n * Groups {@link DropdownMenuRadioItem} children so they behave as a\n * single-select set. Controlled via `value`/`onValueChange`.\n *\n * @example\n * ```tsx\n * <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>\n * <DropdownMenuRadioItem value=\"newest\">Newest first</DropdownMenuRadioItem>\n * <DropdownMenuRadioItem value=\"oldest\">Oldest first</DropdownMenuRadioItem>\n * </DropdownMenuRadioGroup>\n * ```\n *\n * Requires Radix menu context — not supported inside a `variant=\"sheet\"` menu.\n */\nexport const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\n/** Height preset for a {@link DropdownMenuRadioItem}. */\nexport type DropdownMenuRadioItemSize = \"40\";\n\nexport interface DropdownMenuRadioItemProps\n extends React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> {\n /** Optional secondary text shown below the title. */\n helper?: string;\n /** Height of the item row. @default \"40\" */\n size?: DropdownMenuRadioItemSize;\n}\n\n/**\n * A single radio-style choice within a {@link DropdownMenuRadioGroup}. Shows\n * a circular indicator that fills when selected, plus an optional helper line\n * underneath the title.\n */\nexport const DropdownMenuRadioItem = React.forwardRef<\n React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,\n DropdownMenuRadioItemProps\n>(({ className, children, helper, size: _size = \"40\", ...props }, ref) => {\n return (\n <DropdownMenuPrimitive.RadioItem\n ref={ref}\n className={cn(\n \"group flex w-full cursor-pointer items-start gap-3 rounded-xs px-4 py-2 outline-none\",\n \"data-[highlighted]:bg-neutral-alphas-50\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:text-content-disabled\",\n // See DropdownMenuItem above: bg-interaction-hover aliases to the same\n // token as the plain hover background, so it can't distinguish the\n // checked state from an unchecked-but-hovered row.\n \"data-[state=checked]:bg-neutral-alphas-100\",\n \"data-[state=checked]:data-[highlighted]:bg-neutral-alphas-200\",\n className,\n )}\n {...props}\n >\n <span\n className={cn(\n \"mt-1 flex size-4 shrink-0 items-center justify-center rounded-full border border-icons-primary\",\n \"group-data-[disabled]:border-content-disabled\",\n )}\n aria-hidden=\"true\"\n >\n <DropdownMenuPrimitive.ItemIndicator asChild>\n <span className=\"size-2 rounded-full bg-content-primary\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n <span className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <span className=\"typography-body-default-16px-semibold truncate\">{children}</span>\n {helper && (\n <span\n className={cn(\n \"typography-description-12px-regular text-content-secondary\",\n \"group-data-[disabled]:text-content-disabled\",\n )}\n >\n {helper}\n </span>\n )}\n </span>\n </DropdownMenuPrimitive.RadioItem>\n );\n});\nDropdownMenuRadioItem.displayName = \"DropdownMenuRadioItem\";\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { BaseIcon } from "./BaseIcon.mjs";
|
|
5
|
+
const VARIANTS = {
|
|
6
|
+
16: {
|
|
7
|
+
outlined: [
|
|
8
|
+
{
|
|
9
|
+
d: "M5.18 2.333a.667.667 0 0 0-.667-.666H2.5a.667.667 0 0 0-.667.666v2c0 .369.299.667.667.667h2.013a.667.667 0 0 0 .667-.667zm0 4.334A.667.667 0 0 0 4.513 6H2.5a.667.667 0 0 0-.667.667V9c0 .368.299.667.667.667h2.013A.667.667 0 0 0 5.18 9zm0 4.666a.667.667 0 0 0-.667-.666H2.5a.667.667 0 0 0-.667.666v2c0 .368.299.667.667.667h2.013a.667.667 0 0 0 .667-.667zm4.667-9a.667.667 0 0 0-.667-.666H6.847a.667.667 0 0 0-.667.666v2c0 .369.299.667.667.667H9.18a.667.667 0 0 0 .667-.667zm4.32 0a.667.667 0 0 0-.667-.666h-1.987a.667.667 0 0 0-.666.666v2c0 .369.298.667.666.667H13.5a.667.667 0 0 0 .667-.667zm-4.32 4.334A.667.667 0 0 0 9.18 6H6.847a.667.667 0 0 0-.667.667V9c0 .368.299.667.667.667H9.18A.667.667 0 0 0 9.847 9zm0 4.666a.667.667 0 0 0-.667-.666H6.847a.667.667 0 0 0-.667.666v2c0 .368.299.667.667.667H9.18a.667.667 0 0 0 .667-.667zm4.32 0a.667.667 0 0 0-.667-.666h-1.987a.667.667 0 0 0-.666.666v2c0 .368.298.667.666.667H13.5a.667.667 0 0 0 .667-.667zm0-4.666A.667.667 0 0 0 13.5 6h-1.987a.667.667 0 0 0-.666.667V9c0 .368.298.667.666.667H13.5A.667.667 0 0 0 14.167 9z"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
24: {
|
|
14
|
+
outlined: [
|
|
15
|
+
{
|
|
16
|
+
d: "M7.77 3.5a1 1 0 0 0-1-1H3.75a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.02a1 1 0 0 0 1-1zm0 6.5a1 1 0 0 0-1-1H3.75a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h3.02a1 1 0 0 0 1-1zm0 7a1 1 0 0 0-1-1H3.75a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.02a1 1 0 0 0 1-1zm7-13.5a1 1 0 0 0-1-1h-3.5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1zm6.48 0a1 1 0 0 0-1-1h-2.98a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2.98a1 1 0 0 0 1-1zM14.77 10a1 1 0 0 0-1-1h-3.5a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1zm0 7a1 1 0 0 0-1-1h-3.5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1zm6.48 0a1 1 0 0 0-1-1h-2.98a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2.98a1 1 0 0 0 1-1zm0-7a1 1 0 0 0-1-1h-2.98a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h2.98a1 1 0 0 0 1-1z"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
32: {
|
|
21
|
+
outlined: [
|
|
22
|
+
{
|
|
23
|
+
d: "M10.36 4.667c0-.737-.597-1.334-1.333-1.334H5c-.736 0-1.333.597-1.333 1.334v4C3.667 9.403 4.264 10 5 10h4.027c.736 0 1.333-.597 1.333-1.333zm0 8.666c0-.736-.597-1.333-1.333-1.333H5c-.736 0-1.333.597-1.333 1.333V18c0 .736.597 1.333 1.333 1.333h4.027c.736 0 1.333-.597 1.333-1.333zm0 9.334c0-.737-.597-1.334-1.333-1.334H5c-.736 0-1.333.597-1.333 1.334v4C3.667 27.403 4.264 28 5 28h4.027c.736 0 1.333-.597 1.333-1.333zm9.333-18c0-.737-.597-1.334-1.333-1.334h-4.667c-.736 0-1.333.597-1.333 1.334v4c0 .736.597 1.333 1.333 1.333h4.667c.736 0 1.333-.597 1.333-1.333zm8.64 0c0-.737-.597-1.334-1.333-1.334h-3.973c-.737 0-1.334.597-1.334 1.334v4c0 .736.597 1.333 1.334 1.333H27c.736 0 1.333-.597 1.333-1.333zm-8.64 8.666c0-.736-.597-1.333-1.333-1.333h-4.667c-.736 0-1.333.597-1.333 1.333V18c0 .736.597 1.333 1.333 1.333h4.667c.736 0 1.333-.597 1.333-1.333zm0 9.334c0-.737-.597-1.334-1.333-1.334h-4.667c-.736 0-1.333.597-1.333 1.334v4c0 .736.597 1.333 1.333 1.333h4.667c.736 0 1.333-.597 1.333-1.333zm8.64 0c0-.737-.597-1.334-1.333-1.334h-3.973c-.737 0-1.334.597-1.334 1.334v4c0 .736.597 1.333 1.334 1.333H27c.736 0 1.333-.597 1.333-1.333zm0-9.334c0-.736-.597-1.333-1.333-1.333h-3.973c-.737 0-1.334.597-1.334 1.333V18c0 .736.597 1.333 1.334 1.333H27c.736 0 1.333-.597 1.333-1.333z"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const DenseGridViewIcon = React.forwardRef(
|
|
29
|
+
(props, ref) => /* @__PURE__ */ jsx(BaseIcon, { ref, variants: VARIANTS, ...props })
|
|
30
|
+
);
|
|
31
|
+
DenseGridViewIcon.displayName = "DenseGridViewIcon";
|
|
32
|
+
export {
|
|
33
|
+
DenseGridViewIcon
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=DenseGridViewIcon.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DenseGridViewIcon.mjs","sources":["../../../src/components/Icons/DenseGridViewIcon.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { BaseIcon } from \"./BaseIcon\";\nimport type { BaseIconProps, IconVariants } from \"./types\";\n\nconst VARIANTS: IconVariants = {\n 16: {\n outlined: [\n {\n d: \"M5.18 2.333a.667.667 0 0 0-.667-.666H2.5a.667.667 0 0 0-.667.666v2c0 .369.299.667.667.667h2.013a.667.667 0 0 0 .667-.667zm0 4.334A.667.667 0 0 0 4.513 6H2.5a.667.667 0 0 0-.667.667V9c0 .368.299.667.667.667h2.013A.667.667 0 0 0 5.18 9zm0 4.666a.667.667 0 0 0-.667-.666H2.5a.667.667 0 0 0-.667.666v2c0 .368.299.667.667.667h2.013a.667.667 0 0 0 .667-.667zm4.667-9a.667.667 0 0 0-.667-.666H6.847a.667.667 0 0 0-.667.666v2c0 .369.299.667.667.667H9.18a.667.667 0 0 0 .667-.667zm4.32 0a.667.667 0 0 0-.667-.666h-1.987a.667.667 0 0 0-.666.666v2c0 .369.298.667.666.667H13.5a.667.667 0 0 0 .667-.667zm-4.32 4.334A.667.667 0 0 0 9.18 6H6.847a.667.667 0 0 0-.667.667V9c0 .368.299.667.667.667H9.18A.667.667 0 0 0 9.847 9zm0 4.666a.667.667 0 0 0-.667-.666H6.847a.667.667 0 0 0-.667.666v2c0 .368.299.667.667.667H9.18a.667.667 0 0 0 .667-.667zm4.32 0a.667.667 0 0 0-.667-.666h-1.987a.667.667 0 0 0-.666.666v2c0 .368.298.667.666.667H13.5a.667.667 0 0 0 .667-.667zm0-4.666A.667.667 0 0 0 13.5 6h-1.987a.667.667 0 0 0-.666.667V9c0 .368.298.667.666.667H13.5A.667.667 0 0 0 14.167 9z\",\n },\n ],\n },\n 24: {\n outlined: [\n {\n d: \"M7.77 3.5a1 1 0 0 0-1-1H3.75a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.02a1 1 0 0 0 1-1zm0 6.5a1 1 0 0 0-1-1H3.75a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h3.02a1 1 0 0 0 1-1zm0 7a1 1 0 0 0-1-1H3.75a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.02a1 1 0 0 0 1-1zm7-13.5a1 1 0 0 0-1-1h-3.5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1zm6.48 0a1 1 0 0 0-1-1h-2.98a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2.98a1 1 0 0 0 1-1zM14.77 10a1 1 0 0 0-1-1h-3.5a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1zm0 7a1 1 0 0 0-1-1h-3.5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3.5a1 1 0 0 0 1-1zm6.48 0a1 1 0 0 0-1-1h-2.98a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h2.98a1 1 0 0 0 1-1zm0-7a1 1 0 0 0-1-1h-2.98a1 1 0 0 0-1 1v3.5a1 1 0 0 0 1 1h2.98a1 1 0 0 0 1-1z\",\n },\n ],\n },\n 32: {\n outlined: [\n {\n d: \"M10.36 4.667c0-.737-.597-1.334-1.333-1.334H5c-.736 0-1.333.597-1.333 1.334v4C3.667 9.403 4.264 10 5 10h4.027c.736 0 1.333-.597 1.333-1.333zm0 8.666c0-.736-.597-1.333-1.333-1.333H5c-.736 0-1.333.597-1.333 1.333V18c0 .736.597 1.333 1.333 1.333h4.027c.736 0 1.333-.597 1.333-1.333zm0 9.334c0-.737-.597-1.334-1.333-1.334H5c-.736 0-1.333.597-1.333 1.334v4C3.667 27.403 4.264 28 5 28h4.027c.736 0 1.333-.597 1.333-1.333zm9.333-18c0-.737-.597-1.334-1.333-1.334h-4.667c-.736 0-1.333.597-1.333 1.334v4c0 .736.597 1.333 1.333 1.333h4.667c.736 0 1.333-.597 1.333-1.333zm8.64 0c0-.737-.597-1.334-1.333-1.334h-3.973c-.737 0-1.334.597-1.334 1.334v4c0 .736.597 1.333 1.334 1.333H27c.736 0 1.333-.597 1.333-1.333zm-8.64 8.666c0-.736-.597-1.333-1.333-1.333h-4.667c-.736 0-1.333.597-1.333 1.333V18c0 .736.597 1.333 1.333 1.333h4.667c.736 0 1.333-.597 1.333-1.333zm0 9.334c0-.737-.597-1.334-1.333-1.334h-4.667c-.736 0-1.333.597-1.333 1.334v4c0 .736.597 1.333 1.333 1.333h4.667c.736 0 1.333-.597 1.333-1.333zm8.64 0c0-.737-.597-1.334-1.333-1.334h-3.973c-.737 0-1.334.597-1.334 1.334v4c0 .736.597 1.333 1.334 1.333H27c.736 0 1.333-.597 1.333-1.333zm0-9.334c0-.736-.597-1.333-1.333-1.333h-3.973c-.737 0-1.334.597-1.334 1.333V18c0 .736.597 1.333 1.334 1.333H27c.736 0 1.333-.597 1.333-1.333z\",\n },\n ],\n },\n};\n\n/** Props for {@link DenseGridViewIcon}. See {@link BaseIconProps} for the shared shape. */\nexport type DenseGridViewIconProps = BaseIconProps;\n\n/**\n * Dense grid view icon (3×3 tiles) — the compact-grid counterpart of\n * {@link GridViewIcon}. Renders at sizes 16, 24, or 32 px.\n *\n * @example\n * ```tsx\n * <DenseGridViewIcon size={24} />\n * ```\n */\nexport const DenseGridViewIcon = React.forwardRef<SVGSVGElement, DenseGridViewIconProps>(\n (props, ref) => <BaseIcon ref={ref} variants={VARIANTS} {...props} />,\n);\n\nDenseGridViewIcon.displayName = \"DenseGridViewIcon\";\n"],"names":[],"mappings":";;;;AAIA,MAAM,WAAyB;AAAA,EAC7B,IAAI;AAAA,IACF,UAAU;AAAA,MACR;AAAA,QACE,GAAG;AAAA,MAAA;AAAA,IACL;AAAA,EACF;AAAA,EAEF,IAAI;AAAA,IACF,UAAU;AAAA,MACR;AAAA,QACE,GAAG;AAAA,MAAA;AAAA,IACL;AAAA,EACF;AAAA,EAEF,IAAI;AAAA,IACF,UAAU;AAAA,MACR;AAAA,QACE,GAAG;AAAA,MAAA;AAAA,IACL;AAAA,EACF;AAEJ;AAcO,MAAM,oBAAoB,MAAM;AAAA,EACrC,CAAC,OAAO,QAAQ,oBAAC,YAAS,KAAU,UAAU,UAAW,GAAG,MAAA,CAAO;AACrE;AAEA,kBAAkB,cAAc;"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../utils/cn.mjs";
|
|
5
|
+
import { CloseIcon } from "../Icons/CloseIcon.mjs";
|
|
6
|
+
import { EyeOffIcon } from "../Icons/EyeOffIcon.mjs";
|
|
7
|
+
import { FlagIcon } from "../Icons/FlagIcon.mjs";
|
|
8
|
+
const STATUS_CONFIG = {
|
|
9
|
+
default: {
|
|
10
|
+
container: "bg-warning-surface text-warning-content",
|
|
11
|
+
icon: /* @__PURE__ */ jsx(FlagIcon, { size: 16, filled: true }),
|
|
12
|
+
label: "Flagged"
|
|
13
|
+
},
|
|
14
|
+
removed: {
|
|
15
|
+
container: "border border-error-secondary bg-error-negative-content text-content-always-white",
|
|
16
|
+
icon: /* @__PURE__ */ jsx(CloseIcon, { size: 16 }),
|
|
17
|
+
label: "Removed"
|
|
18
|
+
},
|
|
19
|
+
sensitive: {
|
|
20
|
+
container: "bg-buttons-overlay-default text-content-always-white",
|
|
21
|
+
icon: /* @__PURE__ */ jsx(EyeOffIcon, { size: 16, filled: true }),
|
|
22
|
+
label: "Sensitive content"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const MediaStatusIndicator = React.forwardRef(
|
|
26
|
+
({ status = "default", label, className, ...props }, ref) => {
|
|
27
|
+
const config = STATUS_CONFIG[status];
|
|
28
|
+
return /* @__PURE__ */ jsx(
|
|
29
|
+
"span",
|
|
30
|
+
{
|
|
31
|
+
ref,
|
|
32
|
+
role: "img",
|
|
33
|
+
"aria-label": props["aria-label"] ?? label ?? config.label,
|
|
34
|
+
className: cn(
|
|
35
|
+
"inline-flex items-center justify-center rounded-full p-2 [&>svg]:size-4",
|
|
36
|
+
config.container,
|
|
37
|
+
className
|
|
38
|
+
),
|
|
39
|
+
...props,
|
|
40
|
+
children: config.icon
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
MediaStatusIndicator.displayName = "MediaStatusIndicator";
|
|
46
|
+
export {
|
|
47
|
+
MediaStatusIndicator
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=MediaStatusIndicator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaStatusIndicator.mjs","sources":["../../../src/components/MediaStatusIndicator/MediaStatusIndicator.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\nimport { EyeOffIcon } from \"../Icons/EyeOffIcon\";\nimport { FlagIcon } from \"../Icons/FlagIcon\";\n\n/**\n * State represented by the indicator.\n * - `\"default\"`: media flagged / pending review (amber).\n * - `\"removed\"`: media removed for a policy violation (red).\n * - `\"sensitive\"`: sensitive content hidden behind an overlay (dark).\n */\nexport type MediaStatusIndicatorStatus = \"default\" | \"removed\" | \"sensitive\";\n\nexport interface MediaStatusIndicatorProps\n extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Which media state to represent. @default \"default\" */\n status?: MediaStatusIndicatorStatus;\n /**\n * Accessible label announced by assistive tech. Defaults to a per-status\n * description; pass your own for localisation or extra context.\n */\n label?: string;\n}\n\ntype StatusConfig = { container: string; icon: React.ReactNode; label: string };\n\nconst STATUS_CONFIG: Record<MediaStatusIndicatorStatus, StatusConfig> = {\n default: {\n container: \"bg-warning-surface text-warning-content\",\n icon: <FlagIcon size={16} filled />,\n label: \"Flagged\",\n },\n removed: {\n container: \"border border-error-secondary bg-error-negative-content text-content-always-white\",\n icon: <CloseIcon size={16} />,\n label: \"Removed\",\n },\n sensitive: {\n container: \"bg-buttons-overlay-default text-content-always-white\",\n icon: <EyeOffIcon size={16} filled />,\n label: \"Sensitive content\",\n },\n};\n\n/**\n * A compact circular indicator that surfaces the moderation state of a piece of\n * media — flagged, removed, or sensitive. Typically overlaid on a thumbnail or\n * attachment.\n *\n * Renders as `role=\"img\"` with a descriptive `aria-label`; the inner glyph is\n * decorative.\n *\n * @example\n * ```tsx\n * <MediaStatusIndicator status=\"sensitive\" />\n * <MediaStatusIndicator status=\"removed\" label=\"Removed for violating guidelines\" />\n * ```\n */\nexport const MediaStatusIndicator = React.forwardRef<HTMLSpanElement, MediaStatusIndicatorProps>(\n ({ status = \"default\", label, className, ...props }, ref) => {\n const config = STATUS_CONFIG[status];\n return (\n <span\n ref={ref}\n role=\"img\"\n aria-label={props[\"aria-label\"] ?? label ?? config.label}\n className={cn(\n \"inline-flex items-center justify-center rounded-full p-2 [&>svg]:size-4\",\n config.container,\n className,\n )}\n {...props}\n >\n {config.icon}\n </span>\n );\n },\n);\n\nMediaStatusIndicator.displayName = \"MediaStatusIndicator\";\n"],"names":[],"mappings":";;;;;;;AA2BA,MAAM,gBAAkE;AAAA,EACtE,SAAS;AAAA,IACP,WAAW;AAAA,IACX,MAAM,oBAAC,UAAA,EAAS,MAAM,IAAI,QAAM,MAAC;AAAA,IACjC,OAAO;AAAA,EAAA;AAAA,EAET,SAAS;AAAA,IACP,WAAW;AAAA,IACX,MAAM,oBAAC,WAAA,EAAU,MAAM,GAAA,CAAI;AAAA,IAC3B,OAAO;AAAA,EAAA;AAAA,EAET,WAAW;AAAA,IACT,WAAW;AAAA,IACX,MAAM,oBAAC,YAAA,EAAW,MAAM,IAAI,QAAM,MAAC;AAAA,IACnC,OAAO;AAAA,EAAA;AAEX;AAgBO,MAAM,uBAAuB,MAAM;AAAA,EACxC,CAAC,EAAE,SAAS,WAAW,OAAO,WAAW,GAAG,MAAA,GAAS,QAAQ;AAC3D,UAAM,SAAS,cAAc,MAAM;AACnC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAK;AAAA,QACL,cAAY,MAAM,YAAY,KAAK,SAAS,OAAO;AAAA,QACnD,WAAW;AAAA,UACT;AAAA,UACA,OAAO;AAAA,UACP;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA,OAAO;AAAA,MAAA;AAAA,IAAA;AAAA,EAGd;AACF;AAEA,qBAAqB,cAAc;"}
|
|
@@ -78,7 +78,7 @@ const Select = React.forwardRef(
|
|
|
78
78
|
"aria-describedby": bottomText ? helperTextId : void 0,
|
|
79
79
|
"aria-invalid": error || void 0,
|
|
80
80
|
className: cn(
|
|
81
|
-
"flex w-full cursor-pointer items-center justify-between rounded-sm border bg-inputs-inputs-primary outline-none
|
|
81
|
+
"flex w-full cursor-pointer items-center justify-between rounded-sm border bg-inputs-inputs-primary outline-none focus-visible:shadow-focus-ring motion-safe:transition-colors",
|
|
82
82
|
TRIGGER_HEIGHT[size],
|
|
83
83
|
TRIGGER_PADDING_X[size],
|
|
84
84
|
TRIGGER_GAP[size],
|
|
@@ -153,22 +153,66 @@ const SelectContent = React.forwardRef(
|
|
|
153
153
|
) })
|
|
154
154
|
);
|
|
155
155
|
SelectContent.displayName = "SelectContent";
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
156
|
+
const ITEM_SIZE_CLASSES = {
|
|
157
|
+
"40": "min-h-10 py-2 typography-body-default-16px-regular",
|
|
158
|
+
"32": "min-h-8 py-[7px] typography-body-small-14px-regular"
|
|
159
|
+
};
|
|
160
|
+
const ITEM_DESCRIPTION_TYPOGRAPHY = {
|
|
161
|
+
"40": "typography-body-small-14px-regular",
|
|
162
|
+
"32": "typography-description-12px-regular"
|
|
163
|
+
};
|
|
164
|
+
function isRenderableNode(node) {
|
|
165
|
+
return node !== null && node !== void 0 && typeof node !== "boolean";
|
|
166
|
+
}
|
|
167
|
+
const SelectItem = React.forwardRef(({ className, children, size, leadingIcon, avatar, description, ...props }, ref) => {
|
|
168
|
+
const { size: triggerSize } = React.useContext(SelectContext);
|
|
169
|
+
const itemSize = size ?? (triggerSize === "32" ? "32" : "40");
|
|
170
|
+
const hasDescription = isRenderableNode(description);
|
|
171
|
+
const hasAvatar = isRenderableNode(avatar);
|
|
172
|
+
const hasLeadingIcon = isRenderableNode(leadingIcon);
|
|
173
|
+
return /* @__PURE__ */ jsxs(
|
|
174
|
+
SelectPrimitive.Item,
|
|
175
|
+
{
|
|
176
|
+
ref,
|
|
177
|
+
className: cn(
|
|
178
|
+
"group relative flex w-full cursor-pointer select-none gap-2 rounded-xs px-3 text-content-primary outline-none",
|
|
179
|
+
hasDescription ? "items-start" : "items-center",
|
|
180
|
+
ITEM_SIZE_CLASSES[itemSize],
|
|
181
|
+
hasAvatar && !hasDescription && itemSize === "32" && "py-1",
|
|
182
|
+
"focus:bg-neutral-alphas-50 data-highlighted:bg-neutral-alphas-50",
|
|
183
|
+
"data-disabled:pointer-events-none data-disabled:text-content-disabled",
|
|
184
|
+
className
|
|
185
|
+
),
|
|
186
|
+
...props,
|
|
187
|
+
children: [
|
|
188
|
+
hasAvatar ? /* @__PURE__ */ jsx("span", { className: "shrink-0", children: avatar }) : hasLeadingIcon && (hasDescription ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center pt-0.5 [&>svg]:size-4", children: leadingIcon }) : /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center [&>svg]:size-4", children: leadingIcon })),
|
|
189
|
+
hasDescription ? /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
|
|
190
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children }) }),
|
|
191
|
+
/* @__PURE__ */ jsx(
|
|
192
|
+
"span",
|
|
193
|
+
{
|
|
194
|
+
className: cn(
|
|
195
|
+
"truncate text-content-secondary group-data-[disabled]:text-content-disabled",
|
|
196
|
+
ITEM_DESCRIPTION_TYPOGRAPHY[itemSize]
|
|
197
|
+
),
|
|
198
|
+
children: description
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children }) }),
|
|
202
|
+
/* @__PURE__ */ jsx(
|
|
203
|
+
SelectPrimitive.ItemIndicator,
|
|
204
|
+
{
|
|
205
|
+
className: cn(
|
|
206
|
+
"ml-auto flex size-4 shrink-0 items-center justify-center",
|
|
207
|
+
hasDescription && "self-start"
|
|
208
|
+
),
|
|
209
|
+
children: /* @__PURE__ */ jsx(CheckIcon, { className: "size-4 text-content-primary group-data-[disabled]:text-content-disabled" })
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
});
|
|
172
216
|
SelectItem.displayName = "SelectItem";
|
|
173
217
|
const SelectGroup = SelectPrimitive.Group;
|
|
174
218
|
SelectGroup.displayName = "SelectGroup";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Select.mjs","sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import * as SelectPrimitive from \"@radix-ui/react-select\";\nimport * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"@/utils/floatingContentCollisionPadding\";\nimport { CheckIcon } from \"../Icons/CheckIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** Select field height in pixels. */\nexport type SelectSize = \"48\" | \"40\" | \"32\";\n\ntype SelectContextValue = {\n size: SelectSize;\n error: boolean;\n disabled?: boolean;\n};\n\nconst SelectContext = React.createContext<SelectContextValue>({\n size: \"48\",\n error: false,\n});\n\nconst TRIGGER_HEIGHT: Record<SelectSize, string> = {\n \"48\": \"h-12\",\n \"40\": \"h-10\",\n \"32\": \"h-8\",\n};\n\nconst TRIGGER_PADDING_X: Record<SelectSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst TRIGGER_GAP: Record<SelectSize, string> = {\n \"48\": \"gap-3\",\n \"40\": \"gap-3\",\n \"32\": \"gap-2\",\n};\n\nconst TRIGGER_TYPOGRAPHY: Record<SelectSize, string> = {\n \"48\": \"typography-body-default-16px-regular\",\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\nexport interface SelectProps extends Omit<SelectPrimitive.SelectProps, \"dir\"> {\n /** Label text displayed above the trigger. Also used as the accessible name. */\n label?: string;\n /** Accessible name applied directly to the trigger button when no visible `label` is provided. */\n \"aria-label\"?: string;\n /** ID of an external element that labels the trigger button. */\n \"aria-labelledby\"?: string;\n /** Helper text displayed below the trigger. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Height of the select field in pixels. @default \"48\" */\n size?: SelectSize;\n /** Whether the field is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the trigger. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Placeholder shown when no value is selected. */\n placeholder?: string;\n /** Icon element displayed at the left side of the trigger. */\n leftIcon?: React.ReactNode;\n /** Whether the field stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Wraps the `className` of the outermost container div. */\n className?: string;\n /** HTML `id` applied to the trigger button. Auto-generated if omitted. */\n id?: string;\n}\n\n/**\n * A select field with optional label, helper/error text, and an icon slot,\n * built on Radix UI Select for full accessibility and keyboard navigation.\n *\n * Pair with {@link SelectContent} and {@link SelectItem} to provide options.\n *\n * @example\n * ```tsx\n * <Select label=\"Country\" placeholder=\"Select a country\">\n * <SelectContent>\n * <SelectItem value=\"us\">United States</SelectItem>\n * <SelectItem value=\"uk\">United Kingdom</SelectItem>\n * </SelectContent>\n * </Select>\n * ```\n */\nexport const Select = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Trigger>,\n SelectProps\n>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n placeholder,\n leftIcon,\n fullWidth = false,\n className,\n id,\n disabled,\n children,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const triggerId = id ?? generatedId;\n const helperTextId = `${triggerId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n return (\n <SelectContext.Provider value={{ size, error, disabled }}>\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={triggerId}\n className=\"typography-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <SelectPrimitive.Root disabled={disabled} {...props}>\n <SelectPrimitive.Trigger\n ref={ref}\n id={triggerId}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-describedby={bottomText ? helperTextId : undefined}\n aria-invalid={error || undefined}\n className={cn(\n \"flex w-full cursor-pointer items-center justify-between rounded-sm border bg-inputs-inputs-primary outline-none motion-safe:transition-colors focus-visible:shadow-focus-ring\",\n TRIGGER_HEIGHT[size],\n TRIGGER_PADDING_X[size],\n TRIGGER_GAP[size],\n TRIGGER_TYPOGRAPHY[size],\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled &&\n !error &&\n \"hover:border-neutral-alphas-400 data-[state=open]:border-neutral-alphas-400\",\n disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <div className=\"flex min-w-0 items-center gap-2\">\n {leftIcon && (\n <span\n className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\"\n data-testid=\"left-icon\"\n >\n {leftIcon}\n </span>\n )}\n <span className=\"min-w-0 flex-1 truncate text-left text-content-primary [&>[data-placeholder]]:text-content-tertiary\">\n <SelectPrimitive.Value placeholder={placeholder} />\n </span>\n </div>\n\n <SelectPrimitive.Icon asChild>\n <ChevronDownIcon />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n\n {children}\n </SelectPrimitive.Root>\n\n {bottomText && (\n <p\n id={helperTextId}\n className={cn(\n \"typography-description-12px-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\n )}\n >\n {bottomText}\n </p>\n )}\n </div>\n </SelectContext.Provider>\n );\n },\n);\n\nSelect.displayName = \"Select\";\n\nexport interface SelectContentProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> {}\n\n/**\n * The dropdown panel rendered inside a portal. Place {@link SelectItem} elements\n * (and optionally {@link SelectGroup} / {@link SelectLabel}) as children.\n */\nexport const SelectContent = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Content>,\n SelectContentProps\n>(\n (\n {\n className,\n children,\n position = \"popper\",\n sideOffset = 4,\n collisionPadding = FLOATING_CONTENT_COLLISION_PADDING,\n style,\n ...props\n },\n ref,\n ) => (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={ref}\n position={position}\n sideOffset={sideOffset}\n collisionPadding={collisionPadding}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n className={cn(\n \"relative w-max min-w-(--radix-select-trigger-width) max-w-(--radix-select-content-available-width) overflow-hidden rounded-sm border border-neutral-alphas-200 bg-background-primary text-content-primary shadow-[0_4px_16px_rgba(0,0,0,0.10)]\",\n \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.Viewport className=\"max-h-[var(--radix-select-content-available-height)] overflow-y-auto p-1\">\n {children}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n ),\n);\n\nSelectContent.displayName = \"SelectContent\";\n\nexport interface SelectItemProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {}\n\n/**\n * An individual option inside {@link SelectContent}.\n */\nexport const SelectItem = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Item>,\n SelectItemProps\n>(({ className, children, ...props }, ref) => (\n <SelectPrimitive.Item\n ref={ref}\n className={cn(\n \"typography-body-default-16px-regular relative flex w-full cursor-pointer select-none items-center gap-2 rounded-xs py-2 pr-2 pl-3 text-content-primary outline-none\",\n \"focus:bg-neutral-alphas-100 data-disabled:pointer-events-none data-disabled:opacity-50\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n <SelectPrimitive.ItemIndicator className=\"ml-auto flex size-4 shrink-0 items-center justify-center\">\n <CheckIcon className=\"size-4 text-content-primary\" />\n </SelectPrimitive.ItemIndicator>\n </SelectPrimitive.Item>\n));\n\nSelectItem.displayName = \"SelectItem\";\n\n/** Props for {@link SelectGroup}. */\nexport type SelectGroupProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Group>;\n\n/**\n * Groups related {@link SelectItem} elements under a {@link SelectLabel}.\n */\nexport const SelectGroup = SelectPrimitive.Group;\nSelectGroup.displayName = \"SelectGroup\";\n\nexport interface SelectLabelProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> {}\n\n/**\n * A non-interactive label shown above a {@link SelectGroup}.\n */\nexport const SelectLabel = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Label>,\n SelectLabelProps\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Label\n ref={ref}\n className={cn(\n \"typography-description-12px-semibold px-3 py-1.5 text-content-secondary\",\n className,\n )}\n {...props}\n />\n));\n\nSelectLabel.displayName = \"SelectLabel\";\n\nexport interface SelectSeparatorProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> {}\n\n/** A horizontal rule that visually separates groups in {@link SelectContent}. */\nexport const SelectSeparator = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Separator>,\n SelectSeparatorProps\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-neutral-alphas-200\", className)}\n {...props}\n />\n));\n\nSelectSeparator.displayName = \"SelectSeparator\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Select.mjs","sources":["../../../src/components/Select/Select.tsx"],"sourcesContent":["import * as SelectPrimitive from \"@radix-ui/react-select\";\nimport * as React from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { FLOATING_CONTENT_COLLISION_PADDING } from \"@/utils/floatingContentCollisionPadding\";\nimport { CheckIcon } from \"../Icons/CheckIcon\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** Select field height in pixels. */\nexport type SelectSize = \"48\" | \"40\" | \"32\";\n\ntype SelectContextValue = {\n size: SelectSize;\n error: boolean;\n disabled?: boolean;\n};\n\nconst SelectContext = React.createContext<SelectContextValue>({\n size: \"48\",\n error: false,\n});\n\nconst TRIGGER_HEIGHT: Record<SelectSize, string> = {\n \"48\": \"h-12\",\n \"40\": \"h-10\",\n \"32\": \"h-8\",\n};\n\nconst TRIGGER_PADDING_X: Record<SelectSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst TRIGGER_GAP: Record<SelectSize, string> = {\n \"48\": \"gap-3\",\n \"40\": \"gap-3\",\n \"32\": \"gap-2\",\n};\n\nconst TRIGGER_TYPOGRAPHY: Record<SelectSize, string> = {\n \"48\": \"typography-body-default-16px-regular\",\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\nexport interface SelectProps extends Omit<SelectPrimitive.SelectProps, \"dir\"> {\n /** Label text displayed above the trigger. Also used as the accessible name. */\n label?: string;\n /** Accessible name applied directly to the trigger button when no visible `label` is provided. */\n \"aria-label\"?: string;\n /** ID of an external element that labels the trigger button. */\n \"aria-labelledby\"?: string;\n /** Helper text displayed below the trigger. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Height of the select field in pixels. @default \"48\" */\n size?: SelectSize;\n /** Whether the field is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the trigger. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Placeholder shown when no value is selected. */\n placeholder?: string;\n /** Icon element displayed at the left side of the trigger. */\n leftIcon?: React.ReactNode;\n /** Whether the field stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Wraps the `className` of the outermost container div. */\n className?: string;\n /** HTML `id` applied to the trigger button. Auto-generated if omitted. */\n id?: string;\n}\n\n/**\n * A select field with optional label, helper/error text, and an icon slot,\n * built on Radix UI Select for full accessibility and keyboard navigation.\n *\n * Pair with {@link SelectContent} and {@link SelectItem} to provide options.\n *\n * @example\n * ```tsx\n * <Select label=\"Country\" placeholder=\"Select a country\">\n * <SelectContent>\n * <SelectItem value=\"us\">United States</SelectItem>\n * <SelectItem value=\"uk\">United Kingdom</SelectItem>\n * </SelectContent>\n * </Select>\n * ```\n */\nexport const Select = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Trigger>,\n SelectProps\n>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n placeholder,\n leftIcon,\n fullWidth = false,\n className,\n id,\n disabled,\n children,\n \"aria-label\": ariaLabel,\n \"aria-labelledby\": ariaLabelledby,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const triggerId = id ?? generatedId;\n const helperTextId = `${triggerId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n return (\n <SelectContext.Provider value={{ size, error, disabled }}>\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={triggerId}\n className=\"typography-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <SelectPrimitive.Root disabled={disabled} {...props}>\n <SelectPrimitive.Trigger\n ref={ref}\n id={triggerId}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n aria-describedby={bottomText ? helperTextId : undefined}\n aria-invalid={error || undefined}\n className={cn(\n \"flex w-full cursor-pointer items-center justify-between rounded-sm border bg-inputs-inputs-primary outline-none focus-visible:shadow-focus-ring motion-safe:transition-colors\",\n TRIGGER_HEIGHT[size],\n TRIGGER_PADDING_X[size],\n TRIGGER_GAP[size],\n TRIGGER_TYPOGRAPHY[size],\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled &&\n !error &&\n \"hover:border-neutral-alphas-400 data-[state=open]:border-neutral-alphas-400\",\n disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <div className=\"flex min-w-0 items-center gap-2\">\n {leftIcon && (\n <span\n className=\"flex size-5 shrink-0 items-center justify-center text-content-secondary\"\n data-testid=\"left-icon\"\n >\n {leftIcon}\n </span>\n )}\n <span className=\"min-w-0 flex-1 truncate text-left text-content-primary [&>[data-placeholder]]:text-content-tertiary\">\n <SelectPrimitive.Value placeholder={placeholder} />\n </span>\n </div>\n\n <SelectPrimitive.Icon asChild>\n <ChevronDownIcon />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n\n {children}\n </SelectPrimitive.Root>\n\n {bottomText && (\n <p\n id={helperTextId}\n className={cn(\n \"typography-description-12px-regular px-2 pt-1 pb-0.5\",\n error ? \"text-error-content\" : \"text-content-secondary\",\n )}\n >\n {bottomText}\n </p>\n )}\n </div>\n </SelectContext.Provider>\n );\n },\n);\n\nSelect.displayName = \"Select\";\n\nexport interface SelectContentProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> {}\n\n/**\n * The dropdown panel rendered inside a portal. Place {@link SelectItem} elements\n * (and optionally {@link SelectGroup} / {@link SelectLabel}) as children.\n */\nexport const SelectContent = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Content>,\n SelectContentProps\n>(\n (\n {\n className,\n children,\n position = \"popper\",\n sideOffset = 4,\n collisionPadding = FLOATING_CONTENT_COLLISION_PADDING,\n style,\n ...props\n },\n ref,\n ) => (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={ref}\n position={position}\n sideOffset={sideOffset}\n collisionPadding={collisionPadding}\n style={{ zIndex: \"var(--fanvue-ui-portal-z-index, 50)\", ...style }}\n className={cn(\n \"relative w-max min-w-(--radix-select-trigger-width) max-w-(--radix-select-content-available-width) overflow-hidden rounded-sm border border-neutral-alphas-200 bg-background-primary text-content-primary shadow-[0_4px_16px_rgba(0,0,0,0.10)]\",\n \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n \"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95\",\n \"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2\",\n className,\n )}\n {...props}\n >\n <SelectPrimitive.Viewport className=\"max-h-[var(--radix-select-content-available-height)] overflow-y-auto p-1\">\n {children}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n ),\n);\n\nSelectContent.displayName = \"SelectContent\";\n\n/** Dropdown row height in pixels, matching the V2 Menu Item spec. */\nexport type SelectItemSize = \"40\" | \"32\";\n\nconst ITEM_SIZE_CLASSES: Record<SelectItemSize, string> = {\n \"40\": \"min-h-10 py-2 typography-body-default-16px-regular\",\n \"32\": \"min-h-8 py-[7px] typography-body-small-14px-regular\",\n};\n\nconst ITEM_DESCRIPTION_TYPOGRAPHY: Record<SelectItemSize, string> = {\n \"40\": \"typography-body-small-14px-regular\",\n \"32\": \"typography-description-12px-regular\",\n};\n\nexport interface SelectItemProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> {\n /** Row height. Defaults to the parent {@link Select} size (`48`/`40` → `40`, `32` → `32`). */\n size?: SelectItemSize;\n /** Icon (or other node) rendered before the label. Ignored when {@link SelectItemProps.avatar} is set. */\n leadingIcon?: React.ReactNode;\n /**\n * Leading avatar rendered in place of {@link SelectItemProps.leadingIcon}, for rows\n * representing a person or account. Pass an `Avatar` sized to `24`. Takes precedence over `leadingIcon`.\n */\n avatar?: React.ReactNode;\n /** Optional secondary text rendered on a second line below the label. */\n description?: React.ReactNode;\n}\n\n/**\n * Whether a `ReactNode` will actually render. Excludes `null`/`undefined` and\n * booleans so short-circuit props (e.g. `description={cond && \"…\"}`) don't flip\n * the item into a two-line/avatar layout when they resolve to `false`.\n */\nfunction isRenderableNode(node: React.ReactNode): boolean {\n return node !== null && node !== undefined && typeof node !== \"boolean\";\n}\n\n/**\n * An individual option inside {@link SelectContent}, following the V2 Menu Item spec.\n *\n * Supports a leading icon or avatar, an optional two-line layout via `description`,\n * and the standard hover / selected / disabled states. The selected row is marked\n * with a trailing check indicator.\n *\n * @example\n * ```tsx\n * <SelectItem value=\"jane\" avatar={<Avatar size={24} fallback=\"JD\" />} description=\"Product designer\">\n * Jane Doe\n * </SelectItem>\n * ```\n */\nexport const SelectItem = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Item>,\n SelectItemProps\n>(({ className, children, size, leadingIcon, avatar, description, ...props }, ref) => {\n const { size: triggerSize } = React.useContext(SelectContext);\n const itemSize: SelectItemSize = size ?? (triggerSize === \"32\" ? \"32\" : \"40\");\n const hasDescription = isRenderableNode(description);\n const hasAvatar = isRenderableNode(avatar);\n const hasLeadingIcon = isRenderableNode(leadingIcon);\n\n return (\n <SelectPrimitive.Item\n ref={ref}\n className={cn(\n \"group relative flex w-full cursor-pointer select-none gap-2 rounded-xs px-3 text-content-primary outline-none\",\n hasDescription ? \"items-start\" : \"items-center\",\n ITEM_SIZE_CLASSES[itemSize],\n hasAvatar && !hasDescription && itemSize === \"32\" && \"py-1\",\n \"focus:bg-neutral-alphas-50 data-highlighted:bg-neutral-alphas-50\",\n \"data-disabled:pointer-events-none data-disabled:text-content-disabled\",\n className,\n )}\n {...props}\n >\n {hasAvatar ? (\n <span className=\"shrink-0\">{avatar}</span>\n ) : (\n hasLeadingIcon &&\n (hasDescription ? (\n <span className=\"flex shrink-0 items-center pt-0.5 [&>svg]:size-4\">{leadingIcon}</span>\n ) : (\n <span className=\"flex shrink-0 items-center [&>svg]:size-4\">{leadingIcon}</span>\n ))\n )}\n\n {hasDescription ? (\n <span className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <span className=\"truncate\">\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </span>\n <span\n className={cn(\n \"truncate text-content-secondary group-data-[disabled]:text-content-disabled\",\n ITEM_DESCRIPTION_TYPOGRAPHY[itemSize],\n )}\n >\n {description}\n </span>\n </span>\n ) : (\n <span className=\"min-w-0 flex-1 truncate\">\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </span>\n )}\n\n <SelectPrimitive.ItemIndicator\n className={cn(\n \"ml-auto flex size-4 shrink-0 items-center justify-center\",\n hasDescription && \"self-start\",\n )}\n >\n <CheckIcon className=\"size-4 text-content-primary group-data-[disabled]:text-content-disabled\" />\n </SelectPrimitive.ItemIndicator>\n </SelectPrimitive.Item>\n );\n});\n\nSelectItem.displayName = \"SelectItem\";\n\n/** Props for {@link SelectGroup}. */\nexport type SelectGroupProps = React.ComponentPropsWithoutRef<typeof SelectPrimitive.Group>;\n\n/**\n * Groups related {@link SelectItem} elements under a {@link SelectLabel}.\n */\nexport const SelectGroup = SelectPrimitive.Group;\nSelectGroup.displayName = \"SelectGroup\";\n\nexport interface SelectLabelProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> {}\n\n/**\n * A non-interactive label shown above a {@link SelectGroup}.\n */\nexport const SelectLabel = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Label>,\n SelectLabelProps\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Label\n ref={ref}\n className={cn(\n \"typography-description-12px-semibold px-3 py-1.5 text-content-secondary\",\n className,\n )}\n {...props}\n />\n));\n\nSelectLabel.displayName = \"SelectLabel\";\n\nexport interface SelectSeparatorProps\n extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> {}\n\n/** A horizontal rule that visually separates groups in {@link SelectContent}. */\nexport const SelectSeparator = React.forwardRef<\n React.ComponentRef<typeof SelectPrimitive.Separator>,\n SelectSeparatorProps\n>(({ className, ...props }, ref) => (\n <SelectPrimitive.Separator\n ref={ref}\n className={cn(\"-mx-1 my-1 h-px bg-neutral-alphas-200\", className)}\n {...props}\n />\n));\n\nSelectSeparator.displayName = \"SelectSeparator\";\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../utils/cn.mjs";
|
|
5
|
+
import { Button } from "../Button/Button.mjs";
|
|
6
|
+
const OLD_PRICE_TYPOGRAPHY = {
|
|
7
|
+
"48": "typography-body-small-14px-regular",
|
|
8
|
+
"40": "typography-body-small-14px-regular",
|
|
9
|
+
"32": "typography-description-12px-regular"
|
|
10
|
+
};
|
|
11
|
+
function warnMissingAccessibleName(hasTextLabel, ariaLabel) {
|
|
12
|
+
if (process.env.NODE_ENV !== "production" && !hasTextLabel && !ariaLabel) {
|
|
13
|
+
console.warn(
|
|
14
|
+
"SubscribeButton: no accessible name could be derived from a string `children`. Pass an `aria-label` so the action and price are announced."
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const SubscribeButton = React.forwardRef(
|
|
19
|
+
({ variant = "primary", size = "48", price, discount, children = "Join now", ...props }, ref) => {
|
|
20
|
+
const labelText = typeof children === "string" ? children : void 0;
|
|
21
|
+
warnMissingAccessibleName(Boolean(labelText?.trim()), props["aria-label"]);
|
|
22
|
+
const accessibleName = props["aria-label"] ?? [labelText, price, discount ? `was ${discount}` : null].filter(Boolean).join(", ");
|
|
23
|
+
return /* @__PURE__ */ jsxs(Button, { ref, variant, size, ...props, "aria-label": accessibleName, children: [
|
|
24
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-left", children }),
|
|
25
|
+
/* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2", "aria-hidden": "true", children: [
|
|
26
|
+
discount && /* @__PURE__ */ jsx("span", { className: cn(OLD_PRICE_TYPOGRAPHY[size], "line-through"), children: discount }),
|
|
27
|
+
/* @__PURE__ */ jsx("span", { children: price })
|
|
28
|
+
] })
|
|
29
|
+
] });
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
SubscribeButton.displayName = "SubscribeButton";
|
|
33
|
+
export {
|
|
34
|
+
SubscribeButton
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=SubscribeButton.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubscribeButton.mjs","sources":["../../../src/components/SubscribeButton/SubscribeButton.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Button, type ButtonProps } from \"../Button/Button\";\n\n/** Visual style variant of the subscribe button. Mirrors the V2 Subscribe Button \"Type\" set (`brand` is Figma's \"Upsell\"). */\nexport type SubscribeButtonVariant = \"primary\" | \"secondary\" | \"tertiary\" | \"outline\" | \"brand\";\n\n/** Height of the subscribe button in pixels. */\nexport type SubscribeButtonSize = \"48\" | \"40\" | \"32\";\n\nexport interface SubscribeButtonProps\n extends Omit<\n ButtonProps,\n \"variant\" | \"size\" | \"price\" | \"discount\" | \"leftIcon\" | \"rightIcon\" | \"asChild\"\n > {\n /** Visual style variant. `brand` is the Figma \"Upsell\" (green) type. @default \"primary\" */\n variant?: SubscribeButtonVariant;\n /** Height of the button in pixels. @default \"48\" */\n size?: SubscribeButtonSize;\n /** Current price shown on the trailing side (e.g. `\"$9.99/mo\"`). */\n price: string;\n /** Previous price rendered struck-through before {@link SubscribeButtonProps.price}. */\n discount?: string;\n /** Leading action label. @default \"Join now\" */\n children?: React.ReactNode;\n}\n\nconst OLD_PRICE_TYPOGRAPHY: Record<SubscribeButtonSize, string> = {\n \"48\": \"typography-body-small-14px-regular\",\n \"40\": \"typography-body-small-14px-regular\",\n \"32\": \"typography-description-12px-regular\",\n};\n\nfunction warnMissingAccessibleName(hasTextLabel: boolean, ariaLabel?: string) {\n if (process.env.NODE_ENV !== \"production\" && !hasTextLabel && !ariaLabel) {\n console.warn(\n \"SubscribeButton: no accessible name could be derived from a string `children`. Pass an `aria-label` so the action and price are announced.\",\n );\n }\n}\n\n/**\n * A subscription / purchase button pairing an action label with the current\n * price and an optional struck-through previous price. Built on {@link Button},\n * so it inherits the same variants, sizes, `negative` dark-surface treatment,\n * `fullWidth`, loading, and disabled behaviour.\n *\n * The accessible name defaults to the label plus pricing (e.g. `\"Join now, $9.99,\n * was $19.99\"`); pass `aria-label` to override.\n *\n * @example\n * ```tsx\n * <SubscribeButton price=\"$9.99/mo\" discount=\"$19.99\" variant=\"brand\" fullWidth>\n * Join now\n * </SubscribeButton>\n * ```\n */\nexport const SubscribeButton = React.forwardRef<HTMLButtonElement, SubscribeButtonProps>(\n ({ variant = \"primary\", size = \"48\", price, discount, children = \"Join now\", ...props }, ref) => {\n const labelText = typeof children === \"string\" ? children : undefined;\n warnMissingAccessibleName(Boolean(labelText?.trim()), props[\"aria-label\"]);\n\n const accessibleName =\n props[\"aria-label\"] ??\n [labelText, price, discount ? `was ${discount}` : null].filter(Boolean).join(\", \");\n\n return (\n <Button ref={ref} variant={variant} size={size} {...props} aria-label={accessibleName}>\n <span className=\"min-w-0 flex-1 truncate text-left\">{children}</span>\n <span className=\"flex shrink-0 items-center gap-2\" aria-hidden=\"true\">\n {discount && (\n <span className={cn(OLD_PRICE_TYPOGRAPHY[size], \"line-through\")}>{discount}</span>\n )}\n <span>{price}</span>\n </span>\n </Button>\n );\n },\n);\n\nSubscribeButton.displayName = \"SubscribeButton\";\n"],"names":[],"mappings":";;;;;AA2BA,MAAM,uBAA4D;AAAA,EAChE,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,SAAS,0BAA0B,cAAuB,WAAoB;AAC5E,MAAI,QAAQ,IAAI,aAAa,gBAAgB,CAAC,gBAAgB,CAAC,WAAW;AACxE,YAAQ;AAAA,MACN;AAAA,IAAA;AAAA,EAEJ;AACF;AAkBO,MAAM,kBAAkB,MAAM;AAAA,EACnC,CAAC,EAAE,UAAU,WAAW,OAAO,MAAM,OAAO,UAAU,WAAW,YAAY,GAAG,MAAA,GAAS,QAAQ;AAC/F,UAAM,YAAY,OAAO,aAAa,WAAW,WAAW;AAC5D,8BAA0B,QAAQ,WAAW,KAAA,CAAM,GAAG,MAAM,YAAY,CAAC;AAEzE,UAAM,iBACJ,MAAM,YAAY,KAClB,CAAC,WAAW,OAAO,WAAW,OAAO,QAAQ,KAAK,IAAI,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAEnF,WACE,qBAAC,UAAO,KAAU,SAAkB,MAAa,GAAG,OAAO,cAAY,gBACrE,UAAA;AAAA,MAAA,oBAAC,QAAA,EAAK,WAAU,qCAAqC,SAAA,CAAS;AAAA,MAC9D,qBAAC,QAAA,EAAK,WAAU,oCAAmC,eAAY,QAC5D,UAAA;AAAA,QAAA,YACC,oBAAC,UAAK,WAAW,GAAG,qBAAqB,IAAI,GAAG,cAAc,GAAI,UAAA,SAAA,CAAS;AAAA,QAE7E,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,MAAA,EAAA,CACf;AAAA,IAAA,GACF;AAAA,EAEJ;AACF;AAEA,gBAAgB,cAAc;"}
|