periplo-ui 3.29.1 → 3.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +2 -1
- package/dist/components/Breadcrumb/Breadcrumb.js +3 -2
- package/dist/components/Breadcrumb/Breadcrumb.js.map +1 -1
- package/dist/components/Calendar/Calendar.js +11 -10
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +2 -0
- package/dist/components/Combobox/Combobox.js +3 -2
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +5 -1
- package/dist/components/DatePicker/DatePicker.js +19 -5
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/InlineMultiSelect/InlineMultiSelect.d.ts +6 -0
- package/dist/components/InlineMultiSelect/InlineMultiSelect.js +22 -18
- package/dist/components/InlineMultiSelect/InlineMultiSelect.js.map +1 -1
- package/package.json +1 -1
|
@@ -61,12 +61,13 @@ declare const BreadcrumbEllipsis: {
|
|
|
61
61
|
* @param basePath - Optional base path to strip from pathname and use as home route
|
|
62
62
|
* @param textTransform - Optional text transformation to apply to breadcrumb text
|
|
63
63
|
*/
|
|
64
|
-
declare const NavigationBreadcrumb: ({ pathname, pathMappings, translation, basePath, textTransform, }: {
|
|
64
|
+
declare const NavigationBreadcrumb: ({ pathname, pathMappings, translation, basePath, textTransform, endSlash, }: {
|
|
65
65
|
pathname: string;
|
|
66
66
|
pathMappings?: Record<string, string>;
|
|
67
67
|
translation?: (key: string) => string;
|
|
68
68
|
basePath?: string;
|
|
69
69
|
textTransform?: TextTransformOption;
|
|
70
|
+
endSlash?: boolean;
|
|
70
71
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
71
72
|
declare const PagesBreadcrumb: ({ pages, currentPage, }: {
|
|
72
73
|
pages: Array<{
|
|
@@ -127,10 +127,11 @@ const NavigationBreadcrumb = ({
|
|
|
127
127
|
pathMappings,
|
|
128
128
|
translation,
|
|
129
129
|
basePath,
|
|
130
|
-
textTransform
|
|
130
|
+
textTransform,
|
|
131
|
+
endSlash = true
|
|
131
132
|
}) => {
|
|
132
133
|
return /* @__PURE__ */ jsx(BreadcrumbRoot, { children: /* @__PURE__ */ jsx(BreadcrumbList, { children: getRoutes(pathname, pathMappings, translation, basePath).map((route, index, array) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
133
|
-
/* @__PURE__ */ jsx(BreadcrumbItem, { children: index === array.length - 1 ? /* @__PURE__ */ jsx(BreadcrumbPage, { textTransform, children: route.name }) : /* @__PURE__ */ jsx(BreadcrumbLink, { href: route.href, textTransform, children: route.name }) }),
|
|
134
|
+
/* @__PURE__ */ jsx(BreadcrumbItem, { children: index === array.length - 1 ? /* @__PURE__ */ jsx(BreadcrumbPage, { textTransform, children: route.name }) : /* @__PURE__ */ jsx(BreadcrumbLink, { href: route.href + (endSlash ? "/" : ""), textTransform, children: route.name }) }),
|
|
134
135
|
index < array.length - 1 && /* @__PURE__ */ jsx(BreadcrumbSeparator, {})
|
|
135
136
|
] }, index)) }) });
|
|
136
137
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Breadcrumb.js","sources":["../../../src/components/Breadcrumb/Breadcrumb.tsx"],"sourcesContent":["import { CaretRight, DotsThree } from '@phosphor-icons/react'\nimport { Slot } from '@radix-ui/react-slot'\nimport * as React from 'react'\n\nimport { Typography } from '../Typography'\n\nimport { cn } from '@/lib/utils'\n\n/**\n * Root container for the breadcrumb navigation.\n * Provides the overall context and ARIA labeling.\n */\nconst BreadcrumbRoot = React.forwardRef<\n HTMLElement,\n React.ComponentPropsWithoutRef<'nav'> & {\n /** Custom separator element to use between breadcrumb items */\n separator?: React.ReactNode\n }\n>(({ ...props }, ref) => <nav ref={ref} aria-label=\"breadcrumb\" {...props} />)\nBreadcrumbRoot.displayName = 'BreadcrumbRoot'\n\n/**\n * Container for breadcrumb items.\n * Handles layout and spacing of the breadcrumb navigation.\n */\nconst BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<'ol'>>(\n ({ className, ...props }, ref) => (\n <ol\n ref={ref}\n className={cn(\n 'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',\n className,\n )}\n {...props}\n />\n ),\n)\nBreadcrumbList.displayName = 'BreadcrumbList'\n\n/**\n * Individual breadcrumb item container.\n * Wraps a link or the current page indicator.\n */\nconst BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<'li'>>(\n ({ className, ...props }, ref) => (\n <li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />\n ),\n)\nBreadcrumbItem.displayName = 'BreadcrumbItem'\n\n/** Text transform options for breadcrumb links */\ntype TextTransformOption = 'capitalize' | 'capitalize-first' | 'uppercase' | 'none'\n\n/** Get class names for text transform styling */\nconst getTextTransformClass = (transform: TextTransformOption): string => {\n switch (transform) {\n case 'capitalize-first':\n return '[&]:has-[::first-letter]:uppercase'\n case 'capitalize':\n return 'capitalize'\n case 'uppercase':\n return 'uppercase'\n default:\n return ''\n }\n}\n\n/**\n * Interactive link element for breadcrumb navigation.\n * Can be rendered as a custom element using asChild.\n */\nconst BreadcrumbLink = React.forwardRef<\n HTMLAnchorElement,\n React.ComponentPropsWithoutRef<'a'> & {\n /** When true, the component will render its children directly instead of wrapping them in an anchor tag */\n asChild?: boolean\n /** The text transformation to apply to the link text */\n textTransform?: TextTransformOption\n }\n>(({ asChild, className, textTransform = 'capitalize-first', children, ...props }, ref) => {\n const Comp = asChild ? Slot : 'a'\n\n // Simplify the capitalization logic using a direct string transformation\n const content =\n textTransform === 'capitalize-first' && typeof children === 'string'\n ? children.charAt(0).toUpperCase() + children.slice(1)\n : children\n\n return (\n <Typography variant=\"title-sm\" weight={'medium'}>\n <Comp\n ref={ref}\n className={cn(\n 'text-neutral-500 transition-colors hover:text-neutral-950',\n textTransform !== 'capitalize-first' && getTextTransformClass(textTransform),\n className,\n )}\n {...props}\n >\n {content}\n </Comp>\n </Typography>\n )\n})\nBreadcrumbLink.displayName = 'BreadcrumbLink'\n\n/**\n * Current page indicator in the breadcrumb.\n * Non-interactive element showing the current location.\n */\nconst BreadcrumbPage = React.forwardRef<\n HTMLSpanElement,\n React.ComponentPropsWithoutRef<'span'> & {\n textTransform?: TextTransformOption\n }\n>(({ className, textTransform = 'capitalize-first', children, ...props }, ref) => {\n const content =\n textTransform === 'capitalize-first' && typeof children === 'string'\n ? children.charAt(0).toUpperCase() + children.slice(1)\n : children\n\n return (\n <Typography variant=\"title-sm\">\n <span\n ref={ref}\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn(\n 'font-medium text-neutral-950',\n textTransform !== 'capitalize-first' && getTextTransformClass(textTransform),\n className,\n )}\n {...props}\n >\n {content}\n </span>\n </Typography>\n )\n})\nBreadcrumbPage.displayName = 'BreadcrumbPage'\n\n/**\n * Separator element between breadcrumb items.\n * Can be customized with different icons or characters.\n */\nconst BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'span'>) => (\n <span aria-hidden=\"true\" className={cn('text-neutral-500 [&>svg]:size-3.5', className)} {...props}>\n {children ?? <CaretRight />}\n </span>\n)\nBreadcrumbSeparator.displayName = 'BreadcrumbSeparator'\n\n/**\n * Ellipsis indicator for truncated breadcrumb paths.\n * Used to show that there are hidden items in the path.\n */\nconst BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (\n <span aria-hidden=\"true\" className={cn('flex h-9 w-9 items-center justify-center', className)} {...props}>\n <DotsThree className=\"h-4 w-4\" />\n <span className=\"sr-only\">More</span>\n </span>\n)\nBreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis'\n\n/**\n * Sanitizes a route string by replacing hyphens with spaces.\n * @param route - The route string to sanitize.\n * @returns The sanitized route string.\n */\nconst sanitizeRoute = (route: string) => {\n return route.replace(/-/g, ' ')\n}\n\n/**\n * Transforms a kebab-case route to a camelCase translation key\n * @param route - The route segment in kebab-case\n * @returns The translation key in camelCase\n */\nconst routeToTranslationKey = (route: string): string => {\n return route\n .split('-')\n .map((word, index) => (index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)))\n .join('')\n}\n\n/**\n * Gets the routes for the breadcrumb navigation.\n * @param pathname - The current pathname.\n * @param pathMappings - Optional object containing custom path mappings.\n * @param translation - Optional translation function.\n * @param basePath - Optional base path to strip from pathname and use as home route.\n * @returns The routes for the breadcrumb navigation.\n */\nconst getRoutes = (\n pathname: string,\n pathMappings?: Record<string, string>,\n translation?: (key: string) => string,\n basePath?: string,\n) => {\n // Strip the base path from pathname if it exists\n let processedPathname = pathname\n if (basePath && pathname.startsWith(basePath)) {\n processedPathname = pathname.slice(basePath.length)\n // Ensure we don't start with a slash after stripping\n if (processedPathname.startsWith('/')) {\n processedPathname = processedPathname.slice(1)\n }\n }\n\n const baseRoutes = processedPathname.split('/').filter(Boolean)\n\n const routes = baseRoutes.map((route, index) => {\n let displayName = route\n\n // Check if there's a custom mapping for this route\n if (pathMappings && pathMappings[route]) {\n displayName = pathMappings[route]\n }\n // If there's a translation function, convert route to translation key\n else if (translation) {\n const translationKey = routeToTranslationKey(route)\n displayName = translation(translationKey)\n }\n // If no mapping or translation function, sanitize the route\n else {\n displayName = sanitizeRoute(route)\n }\n\n const routeSegments = baseRoutes.slice(0, index + 1)\n const routePath = routeSegments.join('/')\n const fullHref = basePath ? `${basePath}/${routePath}` : `/${routePath}`\n\n return {\n name: displayName,\n href: fullHref,\n }\n })\n\n // Handle home translation\n const homeKey = 'home'\n const homeHref = basePath ?? '/'\n routes.unshift({\n name: pathMappings?.[homeKey] ?? translation?.(homeKey) ?? 'Home',\n href: homeHref,\n })\n\n return routes\n}\n\n/**\n * Navigation breadcrumb component.\n * Displays a breadcrumb navigation for the current path.\n * @param pathname - The current pathname\n * @param pathMappings - Optional object containing custom path mappings\n * @param translation - Optional translation function\n * @param basePath - Optional base path to strip from pathname and use as home route\n * @param textTransform - Optional text transformation to apply to breadcrumb text\n */\nconst NavigationBreadcrumb = ({\n pathname,\n pathMappings,\n translation,\n basePath,\n textTransform,\n}: {\n pathname: string\n pathMappings?: Record<string, string>\n translation?: (key: string) => string\n basePath?: string\n textTransform?: TextTransformOption\n}) => {\n return (\n <BreadcrumbRoot>\n <BreadcrumbList>\n {getRoutes(pathname, pathMappings, translation, basePath).map((route, index, array) => (\n <React.Fragment key={index}>\n <BreadcrumbItem>\n {index === array.length - 1 ? (\n <BreadcrumbPage textTransform={textTransform}>{route.name}</BreadcrumbPage>\n ) : (\n <BreadcrumbLink href={route.href} textTransform={textTransform}>\n {route.name}\n </BreadcrumbLink>\n )}\n </BreadcrumbItem>\n {index < array.length - 1 && <BreadcrumbSeparator />}\n </React.Fragment>\n ))}\n </BreadcrumbList>\n </BreadcrumbRoot>\n )\n}\n\nconst PagesBreadcrumb = ({\n pages,\n currentPage,\n}: {\n pages: Array<{ link: string; text: string }>\n currentPage: string\n}) => {\n return (\n <BreadcrumbRoot>\n <BreadcrumbList>\n {pages.map((page) => (\n <BreadcrumbItem key={pages.indexOf(page)}>\n <BreadcrumbLink href={page.link}>{page.text}</BreadcrumbLink>\n <BreadcrumbSeparator />\n </BreadcrumbItem>\n ))}\n <BreadcrumbItem>\n <BreadcrumbPage>{currentPage}</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </BreadcrumbRoot>\n )\n}\n\nexport {\n BreadcrumbRoot,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n NavigationBreadcrumb,\n PagesBreadcrumb,\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Breadcrumb.js","sources":["../../../src/components/Breadcrumb/Breadcrumb.tsx"],"sourcesContent":["import { CaretRight, DotsThree } from '@phosphor-icons/react'\nimport { Slot } from '@radix-ui/react-slot'\nimport * as React from 'react'\n\nimport { Typography } from '../Typography'\n\nimport { cn } from '@/lib/utils'\n\n/**\n * Root container for the breadcrumb navigation.\n * Provides the overall context and ARIA labeling.\n */\nconst BreadcrumbRoot = React.forwardRef<\n HTMLElement,\n React.ComponentPropsWithoutRef<'nav'> & {\n /** Custom separator element to use between breadcrumb items */\n separator?: React.ReactNode\n }\n>(({ ...props }, ref) => <nav ref={ref} aria-label=\"breadcrumb\" {...props} />)\nBreadcrumbRoot.displayName = 'BreadcrumbRoot'\n\n/**\n * Container for breadcrumb items.\n * Handles layout and spacing of the breadcrumb navigation.\n */\nconst BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<'ol'>>(\n ({ className, ...props }, ref) => (\n <ol\n ref={ref}\n className={cn(\n 'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',\n className,\n )}\n {...props}\n />\n ),\n)\nBreadcrumbList.displayName = 'BreadcrumbList'\n\n/**\n * Individual breadcrumb item container.\n * Wraps a link or the current page indicator.\n */\nconst BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<'li'>>(\n ({ className, ...props }, ref) => (\n <li ref={ref} className={cn('inline-flex items-center gap-1.5', className)} {...props} />\n ),\n)\nBreadcrumbItem.displayName = 'BreadcrumbItem'\n\n/** Text transform options for breadcrumb links */\ntype TextTransformOption = 'capitalize' | 'capitalize-first' | 'uppercase' | 'none'\n\n/** Get class names for text transform styling */\nconst getTextTransformClass = (transform: TextTransformOption): string => {\n switch (transform) {\n case 'capitalize-first':\n return '[&]:has-[::first-letter]:uppercase'\n case 'capitalize':\n return 'capitalize'\n case 'uppercase':\n return 'uppercase'\n default:\n return ''\n }\n}\n\n/**\n * Interactive link element for breadcrumb navigation.\n * Can be rendered as a custom element using asChild.\n */\nconst BreadcrumbLink = React.forwardRef<\n HTMLAnchorElement,\n React.ComponentPropsWithoutRef<'a'> & {\n /** When true, the component will render its children directly instead of wrapping them in an anchor tag */\n asChild?: boolean\n /** The text transformation to apply to the link text */\n textTransform?: TextTransformOption\n }\n>(({ asChild, className, textTransform = 'capitalize-first', children, ...props }, ref) => {\n const Comp = asChild ? Slot : 'a'\n\n // Simplify the capitalization logic using a direct string transformation\n const content =\n textTransform === 'capitalize-first' && typeof children === 'string'\n ? children.charAt(0).toUpperCase() + children.slice(1)\n : children\n\n return (\n <Typography variant=\"title-sm\" weight={'medium'}>\n <Comp\n ref={ref}\n className={cn(\n 'text-neutral-500 transition-colors hover:text-neutral-950',\n textTransform !== 'capitalize-first' && getTextTransformClass(textTransform),\n className,\n )}\n {...props}\n >\n {content}\n </Comp>\n </Typography>\n )\n})\nBreadcrumbLink.displayName = 'BreadcrumbLink'\n\n/**\n * Current page indicator in the breadcrumb.\n * Non-interactive element showing the current location.\n */\nconst BreadcrumbPage = React.forwardRef<\n HTMLSpanElement,\n React.ComponentPropsWithoutRef<'span'> & {\n textTransform?: TextTransformOption\n }\n>(({ className, textTransform = 'capitalize-first', children, ...props }, ref) => {\n const content =\n textTransform === 'capitalize-first' && typeof children === 'string'\n ? children.charAt(0).toUpperCase() + children.slice(1)\n : children\n\n return (\n <Typography variant=\"title-sm\">\n <span\n ref={ref}\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn(\n 'font-medium text-neutral-950',\n textTransform !== 'capitalize-first' && getTextTransformClass(textTransform),\n className,\n )}\n {...props}\n >\n {content}\n </span>\n </Typography>\n )\n})\nBreadcrumbPage.displayName = 'BreadcrumbPage'\n\n/**\n * Separator element between breadcrumb items.\n * Can be customized with different icons or characters.\n */\nconst BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<'span'>) => (\n <span aria-hidden=\"true\" className={cn('text-neutral-500 [&>svg]:size-3.5', className)} {...props}>\n {children ?? <CaretRight />}\n </span>\n)\nBreadcrumbSeparator.displayName = 'BreadcrumbSeparator'\n\n/**\n * Ellipsis indicator for truncated breadcrumb paths.\n * Used to show that there are hidden items in the path.\n */\nconst BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (\n <span aria-hidden=\"true\" className={cn('flex h-9 w-9 items-center justify-center', className)} {...props}>\n <DotsThree className=\"h-4 w-4\" />\n <span className=\"sr-only\">More</span>\n </span>\n)\nBreadcrumbEllipsis.displayName = 'BreadcrumbEllipsis'\n\n/**\n * Sanitizes a route string by replacing hyphens with spaces.\n * @param route - The route string to sanitize.\n * @returns The sanitized route string.\n */\nconst sanitizeRoute = (route: string) => {\n return route.replace(/-/g, ' ')\n}\n\n/**\n * Transforms a kebab-case route to a camelCase translation key\n * @param route - The route segment in kebab-case\n * @returns The translation key in camelCase\n */\nconst routeToTranslationKey = (route: string): string => {\n return route\n .split('-')\n .map((word, index) => (index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)))\n .join('')\n}\n\n/**\n * Gets the routes for the breadcrumb navigation.\n * @param pathname - The current pathname.\n * @param pathMappings - Optional object containing custom path mappings.\n * @param translation - Optional translation function.\n * @param basePath - Optional base path to strip from pathname and use as home route.\n * @returns The routes for the breadcrumb navigation.\n */\nconst getRoutes = (\n pathname: string,\n pathMappings?: Record<string, string>,\n translation?: (key: string) => string,\n basePath?: string,\n) => {\n // Strip the base path from pathname if it exists\n let processedPathname = pathname\n if (basePath && pathname.startsWith(basePath)) {\n processedPathname = pathname.slice(basePath.length)\n // Ensure we don't start with a slash after stripping\n if (processedPathname.startsWith('/')) {\n processedPathname = processedPathname.slice(1)\n }\n }\n\n const baseRoutes = processedPathname.split('/').filter(Boolean)\n\n const routes = baseRoutes.map((route, index) => {\n let displayName = route\n\n // Check if there's a custom mapping for this route\n if (pathMappings && pathMappings[route]) {\n displayName = pathMappings[route]\n }\n // If there's a translation function, convert route to translation key\n else if (translation) {\n const translationKey = routeToTranslationKey(route)\n displayName = translation(translationKey)\n }\n // If no mapping or translation function, sanitize the route\n else {\n displayName = sanitizeRoute(route)\n }\n\n const routeSegments = baseRoutes.slice(0, index + 1)\n const routePath = routeSegments.join('/')\n const fullHref = basePath ? `${basePath}/${routePath}` : `/${routePath}`\n\n return {\n name: displayName,\n href: fullHref,\n }\n })\n\n // Handle home translation\n const homeKey = 'home'\n const homeHref = basePath ?? '/'\n routes.unshift({\n name: pathMappings?.[homeKey] ?? translation?.(homeKey) ?? 'Home',\n href: homeHref,\n })\n\n return routes\n}\n\n/**\n * Navigation breadcrumb component.\n * Displays a breadcrumb navigation for the current path.\n * @param pathname - The current pathname\n * @param pathMappings - Optional object containing custom path mappings\n * @param translation - Optional translation function\n * @param basePath - Optional base path to strip from pathname and use as home route\n * @param textTransform - Optional text transformation to apply to breadcrumb text\n */\nconst NavigationBreadcrumb = ({\n pathname,\n pathMappings,\n translation,\n basePath,\n textTransform,\n endSlash = true,\n}: {\n pathname: string\n pathMappings?: Record<string, string>\n translation?: (key: string) => string\n basePath?: string\n textTransform?: TextTransformOption\n endSlash?: boolean\n}) => {\n return (\n <BreadcrumbRoot>\n <BreadcrumbList>\n {getRoutes(pathname, pathMappings, translation, basePath).map((route, index, array) => (\n <React.Fragment key={index}>\n <BreadcrumbItem>\n {index === array.length - 1 ? (\n <BreadcrumbPage textTransform={textTransform}>{route.name}</BreadcrumbPage>\n ) : (\n <BreadcrumbLink href={route.href + (endSlash ? '/' : '')} textTransform={textTransform}>\n {route.name}\n </BreadcrumbLink>\n )}\n </BreadcrumbItem>\n {index < array.length - 1 && <BreadcrumbSeparator />}\n </React.Fragment>\n ))}\n </BreadcrumbList>\n </BreadcrumbRoot>\n )\n}\n\nconst PagesBreadcrumb = ({\n pages,\n currentPage,\n}: {\n pages: Array<{ link: string; text: string }>\n currentPage: string\n}) => {\n return (\n <BreadcrumbRoot>\n <BreadcrumbList>\n {pages.map((page) => (\n <BreadcrumbItem key={pages.indexOf(page)}>\n <BreadcrumbLink href={page.link}>{page.text}</BreadcrumbLink>\n <BreadcrumbSeparator />\n </BreadcrumbItem>\n ))}\n <BreadcrumbItem>\n <BreadcrumbPage>{currentPage}</BreadcrumbPage>\n </BreadcrumbItem>\n </BreadcrumbList>\n </BreadcrumbRoot>\n )\n}\n\nexport {\n BreadcrumbRoot,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n NavigationBreadcrumb,\n PagesBreadcrumb,\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -75,6 +75,7 @@ function Calendar({
|
|
|
75
75
|
{
|
|
76
76
|
showOutsideDays,
|
|
77
77
|
className: cn("rounded-md border p-3 transition-colors", className),
|
|
78
|
+
id: "calendar",
|
|
78
79
|
style: {
|
|
79
80
|
width: `${248.8 * (columnsDisplayed ?? 1)}px`
|
|
80
81
|
},
|
|
@@ -111,13 +112,13 @@ function Calendar({
|
|
|
111
112
|
const { nextMonth, previousMonth, goToMonth } = useDayPicker();
|
|
112
113
|
const isPreviousDisabled = (() => {
|
|
113
114
|
if (navView === "years") {
|
|
114
|
-
return startMonth && differenceInCalendarDays(new Date(displayYears.from - 1, 0, 1), startMonth) < 0
|
|
115
|
+
return (startMonth && differenceInCalendarDays(new Date(displayYears.from - 1, 0, 1), startMonth) < 0) ?? (endMonth && differenceInCalendarDays(new Date(displayYears.from - 1, 0, 1), endMonth) > 0);
|
|
115
116
|
}
|
|
116
117
|
return !previousMonth;
|
|
117
118
|
})();
|
|
118
119
|
const isNextDisabled = (() => {
|
|
119
120
|
if (navView === "years") {
|
|
120
|
-
return startMonth && differenceInCalendarDays(new Date(displayYears.to + 1, 0, 1), startMonth) < 0
|
|
121
|
+
return (startMonth && differenceInCalendarDays(new Date(displayYears.to + 1, 0, 1), startMonth) < 0) ?? (endMonth && differenceInCalendarDays(new Date(displayYears.to + 1, 0, 1), endMonth) > 0);
|
|
121
122
|
}
|
|
122
123
|
return !nextMonth;
|
|
123
124
|
})();
|
|
@@ -192,26 +193,26 @@ function Calendar({
|
|
|
192
193
|
MonthGrid: ({ className: className2, children, ...props2 }) => {
|
|
193
194
|
const { goToMonth, selected } = useDayPicker();
|
|
194
195
|
if (navView === "years") {
|
|
195
|
-
return /* @__PURE__ */ jsx("div", { className: cn("grid grid-cols-4 gap-y-2", className2), ...props2, children: Array.from({ length: displayYears.to - displayYears.from + 1 }, (
|
|
196
|
-
const isBefore = differenceInCalendarDays(new Date(displayYears.from +
|
|
197
|
-
const isAfter = differenceInCalendarDays(new Date(displayYears.from +
|
|
196
|
+
return /* @__PURE__ */ jsx("div", { className: cn("grid grid-cols-4 gap-y-2", className2), ...props2, children: Array.from({ length: displayYears.to - displayYears.from + 1 }, (_noUsed, index) => {
|
|
197
|
+
const isBefore = differenceInCalendarDays(new Date(displayYears.from + index, 11, 31), startMonth) < 0;
|
|
198
|
+
const isAfter = differenceInCalendarDays(new Date(displayYears.from + index, 0, 0), endMonth) > 0;
|
|
198
199
|
const isDisabled = isBefore || isAfter;
|
|
199
200
|
return /* @__PURE__ */ jsx(
|
|
200
201
|
Button,
|
|
201
202
|
{
|
|
202
203
|
className: cn(
|
|
203
204
|
"text-foreground h-7 w-full text-sm font-normal",
|
|
204
|
-
displayYears.from +
|
|
205
|
+
displayYears.from + index === (/* @__PURE__ */ new Date()).getFullYear() && "bg-primary text-primary-foreground font-medium"
|
|
205
206
|
),
|
|
206
207
|
variant: "text",
|
|
207
208
|
onClick: () => {
|
|
208
209
|
setNavView("days");
|
|
209
|
-
goToMonth(new Date(displayYears.from +
|
|
210
|
+
goToMonth(new Date(displayYears.from + index, selected?.getMonth() ?? 0));
|
|
210
211
|
},
|
|
211
|
-
disabled:
|
|
212
|
-
children: displayYears.from +
|
|
212
|
+
disabled: isDisabled,
|
|
213
|
+
children: displayYears.from + index
|
|
213
214
|
},
|
|
214
|
-
|
|
215
|
+
index
|
|
215
216
|
);
|
|
216
217
|
}) });
|
|
217
218
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Calendar.js","sources":["../../../src/components/Calendar/Calendar.tsx"],"sourcesContent":["'use client'\n\nimport { CaretLeft, CaretRight } from '@phosphor-icons/react'\nimport { differenceInCalendarDays } from 'date-fns'\nimport * as React from 'react'\nimport { DayPicker, labelNext, labelPrevious, useDayPicker, type DayPickerProps } from 'react-day-picker'\n\nimport { Button, buttonVariants } from '../Button'\n\nimport { cn } from '@/lib/utils'\n\nexport type CalendarProps = DayPickerProps & {\n /**\n * In the year view, the number of years to display at once.\n * @default 12\n */\n yearRange?: number\n\n /**\n * Wether to show the year switcher in the caption.\n * @default true\n */\n showYearSwitcher?: boolean\n\n monthsClassName?: string\n monthCaptionClassName?: string\n weekdaysClassName?: string\n weekdayClassName?: string\n monthClassName?: string\n captionClassName?: string\n captionLabelClassName?: string\n buttonNextClassName?: string\n buttonPreviousClassName?: string\n navClassName?: string\n monthGridClassName?: string\n weekClassName?: string\n dayClassName?: string\n dayButtonClassName?: string\n rangeStartClassName?: string\n rangeEndClassName?: string\n selectedClassName?: string\n todayClassName?: string\n outsideClassName?: string\n disabledClassName?: string\n rangeMiddleClassName?: string\n hiddenClassName?: string\n}\n\n/**\n * A custom calendar component built on top of react-day-picker.\n * @param props The props for the calendar.\n * @default yearRange 12\n * @returns\n */\nfunction Calendar({\n className,\n showOutsideDays = true,\n showYearSwitcher = true,\n yearRange = 12,\n numberOfMonths,\n ...props\n}: CalendarProps) {\n const [navView, setNavView] = React.useState<'days' | 'years'>('days')\n const [displayYears, setDisplayYears] = React.useState<{\n from: number\n to: number\n }>(\n React.useMemo(() => {\n const currentYear = new Date().getFullYear()\n return {\n from: currentYear - Math.floor(yearRange / 2 - 1),\n to: currentYear + Math.ceil(yearRange / 2),\n }\n }, [yearRange]),\n )\n\n const { onNextClick, onPrevClick, startMonth, endMonth } = props\n\n const columnsDisplayed = navView === 'years' ? 1 : numberOfMonths\n\n const _monthsClassName = cn('relative flex', props.monthsClassName)\n const _monthCaptionClassName = cn('relative mx-10 flex h-7 items-center justify-center', props.monthCaptionClassName)\n const _weekdaysClassName = cn('flex flex-row', props.weekdaysClassName)\n const _weekdayClassName = cn('w-8 text-sm font-normal text-muted-foreground', props.weekdayClassName)\n const _monthClassName = cn('w-full', props.monthClassName)\n const _captionClassName = cn('relative flex items-center justify-center pt-1', props.captionClassName)\n const _captionLabelClassName = cn('truncate text-sm font-medium', props.captionLabelClassName)\n const buttonNavClassName = buttonVariants({\n variant: 'text',\n className: 'absolute h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',\n })\n const _buttonNextClassName = cn(buttonNavClassName, 'right-0', props.buttonNextClassName)\n const _buttonPreviousClassName = cn(buttonNavClassName, 'left-0', props.buttonPreviousClassName)\n const _navClassName = cn('flex items-start', props.navClassName)\n const _monthGridClassName = cn('mx-auto mt-4', props.monthGridClassName)\n const _weekClassName = cn('mt-2 flex w-max items-start', props.weekClassName)\n const _dayClassName = cn('flex size-8 flex-1 items-center justify-center p-0 text-sm', props.dayClassName)\n const _dayButtonClassName = cn(\n buttonVariants({ variant: 'text' }),\n 'size-8 rounded-md p-0 font-normal transition-none aria-selected:opacity-100',\n props.dayButtonClassName,\n )\n const buttonRangeClassName =\n 'bg-primary [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground'\n const _rangeStartClassName = cn(buttonRangeClassName, 'day-range-start rounded-s-md', props.rangeStartClassName)\n const _rangeEndClassName = cn(buttonRangeClassName, 'day-range-end rounded-e-md', props.rangeEndClassName)\n const _rangeMiddleClassName = cn(\n 'bg-background-300 !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground',\n props.rangeMiddleClassName,\n )\n const _selectedClassName = cn(\n '[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground',\n props.selectedClassName,\n )\n const _todayClassName = cn(\n '[&>button]:bg-background-200 [&>button]:hover:bg-background-200 [&>button]:text-primary-foreground',\n props.todayClassName,\n )\n const _outsideClassName = cn(\n 'day-outside text-muted-foreground opacity-50 aria-selected:text-muted-foreground',\n props.outsideClassName,\n )\n const _disabledClassName = cn('text-muted-foreground opacity-50', props.disabledClassName)\n const _hiddenClassName = cn('invisible flex-1', props.hiddenClassName)\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n className={cn('rounded-md border p-3 transition-colors', className)}\n style={{\n width: `${248.8 * (columnsDisplayed ?? 1)}px`,\n }}\n classNames={{\n months: _monthsClassName,\n month_caption: _monthCaptionClassName,\n weekdays: _weekdaysClassName,\n weekday: _weekdayClassName,\n month: _monthClassName,\n caption: _captionClassName,\n caption_label: _captionLabelClassName,\n button_next: _buttonNextClassName,\n button_previous: _buttonPreviousClassName,\n nav: _navClassName,\n month_grid: _monthGridClassName,\n week: _weekClassName,\n day: _dayClassName,\n day_button: _dayButtonClassName,\n range_start: _rangeStartClassName,\n range_middle: _rangeMiddleClassName,\n range_end: _rangeEndClassName,\n selected: _selectedClassName,\n today: _todayClassName,\n outside: _outsideClassName,\n disabled: _disabledClassName,\n hidden: _hiddenClassName,\n }}\n components={{\n Chevron: ({ orientation }) => {\n const Icon = orientation === 'left' ? CaretLeft : CaretRight\n return <Icon className=\"h-4 w-4\" />\n },\n Nav: ({ className }) => {\n const { nextMonth, previousMonth, goToMonth } = useDayPicker()\n\n const isPreviousDisabled = (() => {\n if (navView === 'years') {\n return (\n (startMonth && differenceInCalendarDays(new Date(displayYears.from - 1, 0, 1), startMonth) < 0) ||\n (endMonth && differenceInCalendarDays(new Date(displayYears.from - 1, 0, 1), endMonth) > 0)\n )\n }\n return !previousMonth\n })()\n\n const isNextDisabled = (() => {\n if (navView === 'years') {\n return (\n (startMonth && differenceInCalendarDays(new Date(displayYears.to + 1, 0, 1), startMonth) < 0) ||\n (endMonth && differenceInCalendarDays(new Date(displayYears.to + 1, 0, 1), endMonth) > 0)\n )\n }\n return !nextMonth\n })()\n\n const handlePreviousClick = React.useCallback(() => {\n if (!previousMonth) return\n if (navView === 'years') {\n setDisplayYears((prev) => ({\n from: prev.from - (prev.to - prev.from + 1),\n to: prev.to - (prev.to - prev.from + 1),\n }))\n onPrevClick?.(new Date(displayYears.from - (displayYears.to - displayYears.from), 0, 1))\n return\n }\n goToMonth(previousMonth)\n onPrevClick?.(previousMonth)\n }, [previousMonth, goToMonth])\n\n const handleNextClick = React.useCallback(() => {\n if (!nextMonth) return\n if (navView === 'years') {\n setDisplayYears((prev) => ({\n from: prev.from + (prev.to - prev.from + 1),\n to: prev.to + (prev.to - prev.from + 1),\n }))\n onNextClick?.(new Date(displayYears.from + (displayYears.to - displayYears.from), 0, 1))\n return\n }\n goToMonth(nextMonth)\n onNextClick?.(nextMonth)\n }, [goToMonth, nextMonth])\n return (\n <nav className={cn('flex items-center', className)}>\n <Button\n variant=\"text\"\n className=\"absolute left-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100\"\n type=\"button\"\n tabIndex={isPreviousDisabled ? undefined : -1}\n disabled={isPreviousDisabled}\n aria-label={\n navView === 'years'\n ? `Go to the previous ${displayYears.to - displayYears.from + 1} years`\n : labelPrevious(previousMonth)\n }\n onClick={handlePreviousClick}\n >\n <CaretLeft className=\"h-4 w-4\" />\n </Button>\n\n <Button\n variant=\"text\"\n className=\"absolute right-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100\"\n type=\"button\"\n tabIndex={isNextDisabled ? undefined : -1}\n disabled={isNextDisabled}\n aria-label={\n navView === 'years'\n ? `Go to the next ${displayYears.to - displayYears.from + 1} years`\n : labelNext(nextMonth)\n }\n onClick={handleNextClick}\n >\n <CaretRight className=\"h-4 w-4\" />\n </Button>\n </nav>\n )\n },\n CaptionLabel: ({ children, ...props }) => {\n if (!showYearSwitcher) return <span {...props}>{children}</span>\n return (\n <Button\n className=\"h-7 w-full truncate text-sm font-medium\"\n variant=\"text\"\n size=\"sm\"\n onClick={() => setNavView((prev) => (prev === 'days' ? 'years' : 'days'))}\n >\n {navView === 'days' ? children : `${displayYears.from} - ${displayYears.to}`}\n </Button>\n )\n },\n MonthGrid: ({ className, children, ...props }) => {\n const { goToMonth, selected } = useDayPicker()\n if (navView === 'years') {\n return (\n <div className={cn('grid grid-cols-4 gap-y-2', className)} {...props}>\n {Array.from({ length: displayYears.to - displayYears.from + 1 }, (_, i) => {\n const isBefore = differenceInCalendarDays(new Date(displayYears.from + i, 11, 31), startMonth!) < 0\n\n const isAfter = differenceInCalendarDays(new Date(displayYears.from + i, 0, 0), endMonth!) > 0\n\n const isDisabled = isBefore || isAfter\n return (\n <Button\n key={i}\n className={cn(\n 'text-foreground h-7 w-full text-sm font-normal',\n displayYears.from + i === new Date().getFullYear() &&\n 'bg-primary text-primary-foreground font-medium',\n )}\n variant=\"text\"\n onClick={() => {\n setNavView('days')\n goToMonth(new Date(displayYears.from + i, (selected as Date | undefined)?.getMonth() ?? 0))\n }}\n disabled={navView === 'years' ? isDisabled : undefined}\n >\n {displayYears.from + i}\n </Button>\n )\n })}\n </div>\n )\n }\n return (\n <table className={className} {...props}>\n {children}\n </table>\n )\n },\n }}\n numberOfMonths={columnsDisplayed}\n {...props}\n />\n )\n}\nCalendar.displayName = 'Calendar'\n\nexport { Calendar }\n"],"names":[],"mappings":";;;;;;;;;AAsDA;AAAkB;AAChB;AACkB;AACC;AACP;AACZ;AAEF;AACE;AACA;AAA8C;AAK1C;AACA;AAAO;AAC2C;AACP;AAC3C;AACY;AAGhB;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAA0C;AAC/B;AACE;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;AAA4B;AACQ;AAClC;AACM;AAER;AAEA;AACA;AACA;AAA8B;AAC5B;AACM;AAER;AAA2B;AACzB;AACM;AAER;AAAwB;AACtB;AACM;AAER;AAA0B;AACxB;AACM;AAER;AACA;AAEA;AACE;AAAC;AAAA;AACC;AACkE;AAC3D;AACoC;AAC3C;AACY;AACF;AACO;AACL;AACD;AACF;AACE;AACM;AACF;AACI;AACZ;AACO;AACN;AACD;AACO;AACC;AACC;AACH;AACD;AACH;AACE;AACC;AACF;AACV;AACY;AAER;AACA;AAAiC;AACnC;AAEE;AAEA;AACE;AACE;AAE2F;AAG7F;AAAQ;AAGV;AACE;AACE;AAEyF;AAG3F;AAAQ;AAGV;AACE;AACA;AACE;AAA2B;AACgB;AACJ;AAEvC;AACA;AAAA;AAEF;AACA;AAA2B;AAG7B;AACE;AACA;AACE;AAA2B;AACgB;AACJ;AAEvC;AACA;AAAA;AAEF;AACA;AAAuB;AAEzB;AAEI;AAAA;AAAC;AAAA;AACS;AACE;AACL;AACsC;AACjC;AAIuB;AAExB;AAEsB;AAAA;AACjC;AAEA;AAAC;AAAA;AACS;AACE;AACL;AACkC;AAC7B;AAIe;AAEhB;AAEuB;AAAA;AAClC;AACF;AAEJ;AAEE;AACA;AACE;AAAC;AAAA;AACW;AACF;AACH;AACmE;AAEE;AAAA;AAC5E;AAEJ;AAEE;AACA;AACE;AAGM;AAEA;AAEA;AACA;AACE;AAAC;AAAA;AAEY;AACT;AAEE;AACJ;AACQ;AAEN;AACA;AAA0F;AAC5F;AAC6C;AAExB;AAAA;AAbhB;AAcP;AAGN;AAGJ;AAGE;AAEJ;AACF;AACgB;AACZ;AAAA;AAGV;AACA;;"}
|
|
1
|
+
{"version":3,"file":"Calendar.js","sources":["../../../src/components/Calendar/Calendar.tsx"],"sourcesContent":["'use client'\n\nimport { CaretLeft, CaretRight } from '@phosphor-icons/react'\nimport { differenceInCalendarDays } from 'date-fns'\nimport * as React from 'react'\nimport { DayPicker, labelNext, labelPrevious, useDayPicker, type DayPickerProps } from 'react-day-picker'\n\nimport { Button, buttonVariants } from '../Button'\n\nimport { cn } from '@/lib/utils'\n\nexport type CalendarProps = DayPickerProps & {\n /**\n * In the year view, the number of years to display at once.\n * @default 12\n */\n yearRange?: number\n\n /**\n * Wether to show the year switcher in the caption.\n * @default true\n */\n showYearSwitcher?: boolean\n\n monthsClassName?: string\n monthCaptionClassName?: string\n weekdaysClassName?: string\n weekdayClassName?: string\n monthClassName?: string\n captionClassName?: string\n captionLabelClassName?: string\n buttonNextClassName?: string\n buttonPreviousClassName?: string\n navClassName?: string\n monthGridClassName?: string\n weekClassName?: string\n dayClassName?: string\n dayButtonClassName?: string\n rangeStartClassName?: string\n rangeEndClassName?: string\n selectedClassName?: string\n todayClassName?: string\n outsideClassName?: string\n disabledClassName?: string\n rangeMiddleClassName?: string\n hiddenClassName?: string\n}\n\n/**\n * A custom calendar component built on top of react-day-picker.\n * @param props The props for the calendar.\n * @default yearRange 12\n * @returns\n */\nfunction Calendar({\n className,\n showOutsideDays = true,\n showYearSwitcher = true,\n yearRange = 12,\n numberOfMonths,\n ...props\n}: CalendarProps) {\n const [navView, setNavView] = React.useState<'days' | 'years'>('days')\n const [displayYears, setDisplayYears] = React.useState<{\n from: number\n to: number\n }>(\n React.useMemo(() => {\n const currentYear = new Date().getFullYear()\n return {\n from: currentYear - Math.floor(yearRange / 2 - 1),\n to: currentYear + Math.ceil(yearRange / 2),\n }\n }, [yearRange]),\n )\n\n const { onNextClick, onPrevClick, startMonth, endMonth } = props\n\n const columnsDisplayed = navView === 'years' ? 1 : numberOfMonths\n\n const _monthsClassName = cn('relative flex', props.monthsClassName)\n const _monthCaptionClassName = cn('relative mx-10 flex h-7 items-center justify-center', props.monthCaptionClassName)\n const _weekdaysClassName = cn('flex flex-row', props.weekdaysClassName)\n const _weekdayClassName = cn('w-8 text-sm font-normal text-muted-foreground', props.weekdayClassName)\n const _monthClassName = cn('w-full', props.monthClassName)\n const _captionClassName = cn('relative flex items-center justify-center pt-1', props.captionClassName)\n const _captionLabelClassName = cn('truncate text-sm font-medium', props.captionLabelClassName)\n const buttonNavClassName = buttonVariants({\n variant: 'text',\n className: 'absolute h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',\n })\n const _buttonNextClassName = cn(buttonNavClassName, 'right-0', props.buttonNextClassName)\n const _buttonPreviousClassName = cn(buttonNavClassName, 'left-0', props.buttonPreviousClassName)\n const _navClassName = cn('flex items-start', props.navClassName)\n const _monthGridClassName = cn('mx-auto mt-4', props.monthGridClassName)\n const _weekClassName = cn('mt-2 flex w-max items-start', props.weekClassName)\n const _dayClassName = cn('flex size-8 flex-1 items-center justify-center p-0 text-sm', props.dayClassName)\n const _dayButtonClassName = cn(\n buttonVariants({ variant: 'text' }),\n 'size-8 rounded-md p-0 font-normal transition-none aria-selected:opacity-100',\n props.dayButtonClassName,\n )\n const buttonRangeClassName =\n 'bg-primary [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground'\n const _rangeStartClassName = cn(buttonRangeClassName, 'day-range-start rounded-s-md', props.rangeStartClassName)\n const _rangeEndClassName = cn(buttonRangeClassName, 'day-range-end rounded-e-md', props.rangeEndClassName)\n const _rangeMiddleClassName = cn(\n 'bg-background-300 !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground',\n props.rangeMiddleClassName,\n )\n const _selectedClassName = cn(\n '[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground',\n props.selectedClassName,\n )\n const _todayClassName = cn(\n '[&>button]:bg-background-200 [&>button]:hover:bg-background-200 [&>button]:text-primary-foreground',\n props.todayClassName,\n )\n const _outsideClassName = cn(\n 'day-outside text-muted-foreground opacity-50 aria-selected:text-muted-foreground',\n props.outsideClassName,\n )\n const _disabledClassName = cn('text-muted-foreground opacity-50', props.disabledClassName)\n const _hiddenClassName = cn('invisible flex-1', props.hiddenClassName)\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n className={cn('rounded-md border p-3 transition-colors', className)}\n id=\"calendar\"\n style={{\n width: `${248.8 * (columnsDisplayed ?? 1)}px`,\n }}\n classNames={{\n months: _monthsClassName,\n month_caption: _monthCaptionClassName,\n weekdays: _weekdaysClassName,\n weekday: _weekdayClassName,\n month: _monthClassName,\n caption: _captionClassName,\n caption_label: _captionLabelClassName,\n button_next: _buttonNextClassName,\n button_previous: _buttonPreviousClassName,\n nav: _navClassName,\n month_grid: _monthGridClassName,\n week: _weekClassName,\n day: _dayClassName,\n day_button: _dayButtonClassName,\n range_start: _rangeStartClassName,\n range_middle: _rangeMiddleClassName,\n range_end: _rangeEndClassName,\n selected: _selectedClassName,\n today: _todayClassName,\n outside: _outsideClassName,\n disabled: _disabledClassName,\n hidden: _hiddenClassName,\n }}\n components={{\n Chevron: ({ orientation }) => {\n const Icon = orientation === 'left' ? CaretLeft : CaretRight\n return <Icon className=\"h-4 w-4\" />\n },\n Nav: ({ className }) => {\n const { nextMonth, previousMonth, goToMonth } = useDayPicker()\n\n const isPreviousDisabled = (() => {\n if (navView === 'years') {\n return (\n (startMonth && differenceInCalendarDays(new Date(displayYears.from - 1, 0, 1), startMonth) < 0) ??\n (endMonth && differenceInCalendarDays(new Date(displayYears.from - 1, 0, 1), endMonth) > 0)\n )\n }\n return !previousMonth\n })()\n\n const isNextDisabled = (() => {\n if (navView === 'years') {\n return (\n (startMonth && differenceInCalendarDays(new Date(displayYears.to + 1, 0, 1), startMonth) < 0) ??\n (endMonth && differenceInCalendarDays(new Date(displayYears.to + 1, 0, 1), endMonth) > 0)\n )\n }\n return !nextMonth\n })()\n\n const handlePreviousClick = React.useCallback(() => {\n if (!previousMonth) return\n if (navView === 'years') {\n setDisplayYears((prev) => ({\n from: prev.from - (prev.to - prev.from + 1),\n to: prev.to - (prev.to - prev.from + 1),\n }))\n onPrevClick?.(new Date(displayYears.from - (displayYears.to - displayYears.from), 0, 1))\n return\n }\n goToMonth(previousMonth)\n onPrevClick?.(previousMonth)\n }, [previousMonth, goToMonth])\n\n const handleNextClick = React.useCallback(() => {\n if (!nextMonth) return\n if (navView === 'years') {\n setDisplayYears((prev) => ({\n from: prev.from + (prev.to - prev.from + 1),\n to: prev.to + (prev.to - prev.from + 1),\n }))\n onNextClick?.(new Date(displayYears.from + (displayYears.to - displayYears.from), 0, 1))\n return\n }\n goToMonth(nextMonth)\n onNextClick?.(nextMonth)\n }, [goToMonth, nextMonth])\n return (\n <nav className={cn('flex items-center', className)}>\n <Button\n variant=\"text\"\n className=\"absolute left-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100\"\n type=\"button\"\n tabIndex={isPreviousDisabled ? undefined : -1}\n disabled={isPreviousDisabled}\n aria-label={\n navView === 'years'\n ? `Go to the previous ${displayYears.to - displayYears.from + 1} years`\n : labelPrevious(previousMonth)\n }\n onClick={handlePreviousClick}\n >\n <CaretLeft className=\"h-4 w-4\" />\n </Button>\n\n <Button\n variant=\"text\"\n className=\"absolute right-0 h-7 w-7 bg-transparent p-0 opacity-80 hover:opacity-100\"\n type=\"button\"\n tabIndex={isNextDisabled ? undefined : -1}\n disabled={isNextDisabled}\n aria-label={\n navView === 'years'\n ? `Go to the next ${displayYears.to - displayYears.from + 1} years`\n : labelNext(nextMonth)\n }\n onClick={handleNextClick}\n >\n <CaretRight className=\"h-4 w-4\" />\n </Button>\n </nav>\n )\n },\n CaptionLabel: ({ children, ...props }) => {\n if (!showYearSwitcher) return <span {...props}>{children}</span>\n return (\n <Button\n className=\"h-7 w-full truncate text-sm font-medium\"\n variant=\"text\"\n size=\"sm\"\n onClick={() => setNavView((prev) => (prev === 'days' ? 'years' : 'days'))}\n >\n {navView === 'days' ? children : `${displayYears.from} - ${displayYears.to}`}\n </Button>\n )\n },\n MonthGrid: ({ className, children, ...props }) => {\n const { goToMonth, selected } = useDayPicker()\n if (navView === 'years') {\n return (\n <div className={cn('grid grid-cols-4 gap-y-2', className)} {...props}>\n {Array.from({ length: displayYears.to - displayYears.from + 1 }, (_noUsed, index) => {\n const isBefore =\n differenceInCalendarDays(new Date(displayYears.from + index, 11, 31), startMonth!) < 0\n\n const isAfter = differenceInCalendarDays(new Date(displayYears.from + index, 0, 0), endMonth!) > 0\n\n const isDisabled = isBefore || isAfter\n return (\n <Button\n key={index}\n className={cn(\n 'text-foreground h-7 w-full text-sm font-normal',\n displayYears.from + index === new Date().getFullYear() &&\n 'bg-primary text-primary-foreground font-medium',\n )}\n variant=\"text\"\n onClick={() => {\n setNavView('days')\n goToMonth(new Date(displayYears.from + index, (selected as Date | undefined)?.getMonth() ?? 0))\n }}\n disabled={isDisabled}\n >\n {displayYears.from + index}\n </Button>\n )\n })}\n </div>\n )\n }\n return (\n <table className={className} {...props}>\n {children}\n </table>\n )\n },\n }}\n numberOfMonths={columnsDisplayed}\n {...props}\n />\n )\n}\nCalendar.displayName = 'Calendar'\n\nexport { Calendar }\n"],"names":[],"mappings":";;;;;;;;;AAsDA;AAAkB;AAChB;AACkB;AACC;AACP;AACZ;AAEF;AACE;AACA;AAA8C;AAK1C;AACA;AAAO;AAC2C;AACP;AAC3C;AACY;AAGhB;AAEA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAA0C;AAC/B;AACE;AAEb;AACA;AACA;AACA;AACA;AACA;AACA;AAA4B;AACQ;AAClC;AACM;AAER;AAEA;AACA;AACA;AAA8B;AAC5B;AACM;AAER;AAA2B;AACzB;AACM;AAER;AAAwB;AACtB;AACM;AAER;AAA0B;AACxB;AACM;AAER;AACA;AAEA;AACE;AAAC;AAAA;AACC;AACkE;AAC/D;AACI;AACoC;AAC3C;AACY;AACF;AACO;AACL;AACD;AACF;AACE;AACM;AACF;AACI;AACZ;AACO;AACN;AACD;AACO;AACC;AACC;AACH;AACD;AACH;AACE;AACC;AACF;AACV;AACY;AAER;AACA;AAAiC;AACnC;AAEE;AAEA;AACE;AACE;AAE2F;AAG7F;AAAQ;AAGV;AACE;AACE;AAEyF;AAG3F;AAAQ;AAGV;AACE;AACA;AACE;AAA2B;AACgB;AACJ;AAEvC;AACA;AAAA;AAEF;AACA;AAA2B;AAG7B;AACE;AACA;AACE;AAA2B;AACgB;AACJ;AAEvC;AACA;AAAA;AAEF;AACA;AAAuB;AAEzB;AAEI;AAAA;AAAC;AAAA;AACS;AACE;AACL;AACsC;AACjC;AAIuB;AAExB;AAEsB;AAAA;AACjC;AAEA;AAAC;AAAA;AACS;AACE;AACL;AACkC;AAC7B;AAIe;AAEhB;AAEuB;AAAA;AAClC;AACF;AAEJ;AAEE;AACA;AACE;AAAC;AAAA;AACW;AACF;AACH;AACmE;AAEE;AAAA;AAC5E;AAEJ;AAEE;AACA;AACE;AAGM;AAGA;AAEA;AACA;AACE;AAAC;AAAA;AAEY;AACT;AAEE;AACJ;AACQ;AAEN;AACA;AAA8F;AAChG;AACU;AAEW;AAAA;AAbhB;AAcP;AAGN;AAGJ;AAGE;AAEJ;AACF;AACgB;AACZ;AAAA;AAGV;AACA;;"}
|
|
@@ -35,6 +35,8 @@ type ComboboxBaseProps<T> = {
|
|
|
35
35
|
onClear?: boolean | (() => void);
|
|
36
36
|
/** Whether the selection can be cleared */
|
|
37
37
|
clearable?: boolean;
|
|
38
|
+
/** Whether the combobox is inside a modal */
|
|
39
|
+
modal?: boolean;
|
|
38
40
|
};
|
|
39
41
|
export type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {
|
|
40
42
|
multiple?: false;
|
|
@@ -42,7 +42,8 @@ const Combobox = (props) => {
|
|
|
42
42
|
filterOptions,
|
|
43
43
|
multiple,
|
|
44
44
|
clearable = true,
|
|
45
|
-
onClear
|
|
45
|
+
onClear,
|
|
46
|
+
modal = false
|
|
46
47
|
} = props;
|
|
47
48
|
const [open, setOpen] = useState(false);
|
|
48
49
|
const [searchTerm, setSearchTerm] = useState("");
|
|
@@ -123,7 +124,7 @@ const Combobox = (props) => {
|
|
|
123
124
|
}, [props.value, multiple]);
|
|
124
125
|
const showClearButton = onClear && hasValue && isHovered && clearable;
|
|
125
126
|
return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-1", children: [
|
|
126
|
-
/* @__PURE__ */ jsxs(PopoverRoot, { open, onOpenChange: setOpen, children: [
|
|
127
|
+
/* @__PURE__ */ jsxs(PopoverRoot, { open, onOpenChange: setOpen, modal, children: [
|
|
127
128
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
128
129
|
Button,
|
|
129
130
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.js","sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["import { CaretUpDown, Check, X } from '@phosphor-icons/react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport { cn } from '../../lib/utils'\nimport { Button, buttonVariants } from '../Button'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '../Tooltip'\n\nconst ComboboxOptionWithTooltip = ({ label }: { label: string }) => {\n const textRef = useRef<HTMLSpanElement>(null)\n const [isTruncated, setIsTruncated] = useState(false)\n\n useEffect(() => {\n const frame = requestAnimationFrame(() => {\n if (textRef.current) {\n setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth)\n }\n })\n\n return () => cancelAnimationFrame(frame)\n }, [label])\n\n return (\n <TooltipProvider>\n <TooltipRoot delayDuration={300}>\n <TooltipTrigger asChild>\n <span ref={textRef} className=\"block truncate\">\n {label}\n </span>\n </TooltipTrigger>\n {isTruncated && <TooltipContent>{label}</TooltipContent>}\n </TooltipRoot>\n </TooltipProvider>\n )\n}\n\ntype ComboboxBaseProps<T> = {\n /** Unique identifier for the combobox */\n id?: string\n /** Array of options to display in the combobox */\n options: Array<T>\n /** Function to get the unique identifier from an option. */\n getOptionValue: (option: T) => string\n /** Function to get the display text from an option. */\n getOptionLabel: (option: T) => string\n /** Custom render function for options. If not provided, defaults to showing a checkmark and label */\n renderOption?: (option: T, isSelected: boolean) => React.ReactNode\n /** Placeholder text shown when no option is selected */\n placeholder?: string\n /** Placeholder text for the search input field */\n searchPlaceholder?: string\n /** Message shown when no options match the search query */\n emptyMessage?: string\n /** Additional CSS classes to apply to the combobox trigger */\n className?: string\n /** Whether the combobox is disabled */\n disabled?: boolean\n /** Maximum height of the options list. Can be any valid CSS height value */\n maxHeight?: string | number\n /** Whether to close the dropdown when an option is selected. Defaults to `true` for single select and `false` for multi-select. */\n closeOnSelect?: boolean\n /** Whether the combobox is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the combobox has an error */\n error?: boolean | string\n /** Custom function to filter options based on search term */\n filterOptions?: (options: Array<T>, searchTerm: string) => Array<T>\n /** Callback function executed when the clear button is clicked. When provided, an X button will appear on hover to clear the selection. */\n onClear?: boolean | (() => void)\n /** Whether the selection can be cleared */\n clearable?: boolean\n}\n\nexport type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {\n multiple?: false\n value?: string\n onChange: (value: string) => void\n /** Custom render function for the selected value display. */\n renderLabel?: (selectedOption: T) => React.ReactNode\n}\n\nexport type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {\n multiple: true\n value?: Array<string>\n onChange: (value: Array<string>) => void\n /** Custom render function for the selected value(s) display. */\n renderLabel?: (selectedOptions: Array<T>, onRemove: (value: string) => void) => React.ReactNode\n}\n\nexport type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>\n\n/**\n * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.\n *\n * @example\n * ```tsx\n * interface User {\n * id: string\n * name: string\n * email: string\n * }\n *\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * />\n * ```\n *\n * @example Custom filtering\n * ```tsx\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * filterOptions={(options, searchTerm) =>\n * options.filter(user =>\n * user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n * user.email.toLowerCase().includes(searchTerm.toLowerCase())\n * )\n * }\n * />\n * ```\n */\nexport const Combobox = <T extends object>(props: ComboboxProps<T>) => {\n const {\n id,\n options,\n getOptionValue,\n getOptionLabel,\n placeholder = 'Select...',\n searchPlaceholder = 'Search...',\n emptyMessage = 'No results found.',\n className = 'w-60',\n disabled = false,\n maxHeight = '300px',\n renderOption,\n loading = false,\n loadingPlaceholder = 'Cargando...',\n error = false,\n filterOptions,\n multiple,\n clearable = true,\n onClear,\n } = props\n\n const [open, setOpen] = useState(false)\n const [searchTerm, setSearchTerm] = useState('')\n const [isHovered, setIsHovered] = useState(false)\n\n const closeOnSelect = props.closeOnSelect ?? !props.multiple\n\n const filteredOptions = useMemo(() => {\n if (!filterOptions) {\n return options.filter((option) => getOptionLabel(option).toLowerCase().includes(searchTerm.toLowerCase()))\n }\n\n return searchTerm ? filterOptions(options, searchTerm) : options\n }, [filterOptions, options, searchTerm, getOptionLabel])\n\n const handleSelect = (currentValue: string) => {\n if (multiple) {\n const { value = [], onChange } = props\n const isRemoving = value.includes(currentValue)\n\n // If trying to remove the last item and clearable is false, don't allow it\n if (isRemoving && !clearable && value.length === 1) {\n return\n }\n\n const newValues = isRemoving ? value.filter((val) => val !== currentValue) : [...value, currentValue]\n onChange(newValues)\n } else {\n const { value, onChange } = props\n const newValue = clearable && currentValue === value ? '' : currentValue\n onChange(newValue)\n }\n\n if (closeOnSelect) {\n setOpen(false)\n }\n }\n\n const handleRemove = (valueToRemove: string) => {\n if (props.multiple) {\n const { value = [], onChange } = props\n\n // If trying to remove the last item and clearable is false, don't allow it\n if (!clearable && value.length === 1) {\n return\n }\n\n const newValues = value.filter((val) => val !== valueToRemove)\n onChange(newValues)\n }\n }\n\n const handleClear = (event: React.MouseEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (multiple) {\n const { onChange } = props\n\n // Only clear if clearable is true\n if (clearable) {\n onChange([])\n }\n } else {\n const { onChange } = props\n onChange('')\n }\n\n if (typeof onClear === 'function') {\n onClear()\n }\n }\n\n const displayValue = useMemo(() => {\n if (multiple) {\n const { value = [], renderLabel } = props\n if (value.length === 0) return placeholder\n\n const selectedOptions = options.filter((option) => value.includes(getOptionValue(option)))\n\n if (renderLabel) {\n return renderLabel(selectedOptions, handleRemove)\n }\n\n return selectedOptions.map(getOptionLabel).join(', ')\n }\n\n const { value, renderLabel } = props\n const selectedOption = options.find((option) => getOptionValue(option) === value)\n if (renderLabel && selectedOption) {\n return renderLabel(selectedOption)\n }\n return selectedOption ? getOptionLabel(selectedOption) : placeholder\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props, getOptionLabel, getOptionValue, options, placeholder, multiple])\n\n const hasValue = useMemo(() => {\n if (multiple) {\n return props.value && props.value.length > 0\n }\n return !!props.value\n }, [props.value, multiple])\n\n const showClearButton = onClear && hasValue && isHovered && clearable\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <PopoverRoot open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n id={id}\n type=\"button\"\n disabled={disabled}\n variant=\"text\"\n className={cn(\n buttonVariants({ variant: 'input' }),\n 'relative flex w-full justify-between rounded-lg',\n multiple && 'renderLabel' in props && props.renderLabel ? 'h-auto min-h-10' : 'h-10',\n open && 'border-neutral-950',\n disabled && 'cursor-not-allowed',\n error && 'border-error-400 focus-visible:border-error-700',\n className,\n )}\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <span\n className={cn(\n 'block',\n !hasValue && 'text-neutral-300',\n !(multiple && 'renderLabel' in props && props.renderLabel) && 'truncate',\n )}\n >\n {displayValue}\n </span>\n <CaretUpDown\n className={cn(\n 'h-4 w-4 shrink-0 opacity-50 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-50',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 z-10 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"p-0\">\n <Command shouldFilter={false}>\n <CommandInput\n placeholder={searchPlaceholder}\n disabled={loading}\n value={searchTerm}\n onValueChange={setSearchTerm}\n />\n <CommandList style={{ maxHeight }}>\n {loading ? (\n <div className=\"text-muted-foreground flex items-center justify-center py-6 text-sm\">\n {loadingPlaceholder}\n </div>\n ) : (\n <>\n {filteredOptions.length === 0 && <CommandEmpty>{emptyMessage}</CommandEmpty>}\n <CommandGroup>\n {filteredOptions.map((option) => {\n const optionValue = getOptionValue(option)\n const isSelected = multiple ? props.value?.includes(optionValue) : props.value === optionValue\n\n return (\n <CommandItem key={optionValue} value={optionValue} onSelect={handleSelect}>\n {renderOption ? (\n renderOption(option, isSelected ?? false)\n ) : (\n <>\n <Check className={`mr-2 h-4 w-4 ${isSelected ? 'opacity-100' : 'opacity-0'}`} />\n <ComboboxOptionWithTooltip label={getOptionLabel(option)} />\n </>\n )}\n </CommandItem>\n )\n })}\n </CommandGroup>\n </>\n )}\n </CommandList>\n </Command>\n </PopoverContent>\n </PopoverRoot>\n {typeof error === 'string' && <span className=\"text-error-500 text-sm\">{error}</span>}\n </div>\n )\n}\n"],"names":["value","renderLabel"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Combobox.js","sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["import { CaretUpDown, Check, X } from '@phosphor-icons/react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport { cn } from '../../lib/utils'\nimport { Button, buttonVariants } from '../Button'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '../Tooltip'\n\nconst ComboboxOptionWithTooltip = ({ label }: { label: string }) => {\n const textRef = useRef<HTMLSpanElement>(null)\n const [isTruncated, setIsTruncated] = useState(false)\n\n useEffect(() => {\n const frame = requestAnimationFrame(() => {\n if (textRef.current) {\n setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth)\n }\n })\n\n return () => cancelAnimationFrame(frame)\n }, [label])\n\n return (\n <TooltipProvider>\n <TooltipRoot delayDuration={300}>\n <TooltipTrigger asChild>\n <span ref={textRef} className=\"block truncate\">\n {label}\n </span>\n </TooltipTrigger>\n {isTruncated && <TooltipContent>{label}</TooltipContent>}\n </TooltipRoot>\n </TooltipProvider>\n )\n}\n\ntype ComboboxBaseProps<T> = {\n /** Unique identifier for the combobox */\n id?: string\n /** Array of options to display in the combobox */\n options: Array<T>\n /** Function to get the unique identifier from an option. */\n getOptionValue: (option: T) => string\n /** Function to get the display text from an option. */\n getOptionLabel: (option: T) => string\n /** Custom render function for options. If not provided, defaults to showing a checkmark and label */\n renderOption?: (option: T, isSelected: boolean) => React.ReactNode\n /** Placeholder text shown when no option is selected */\n placeholder?: string\n /** Placeholder text for the search input field */\n searchPlaceholder?: string\n /** Message shown when no options match the search query */\n emptyMessage?: string\n /** Additional CSS classes to apply to the combobox trigger */\n className?: string\n /** Whether the combobox is disabled */\n disabled?: boolean\n /** Maximum height of the options list. Can be any valid CSS height value */\n maxHeight?: string | number\n /** Whether to close the dropdown when an option is selected. Defaults to `true` for single select and `false` for multi-select. */\n closeOnSelect?: boolean\n /** Whether the combobox is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the combobox has an error */\n error?: boolean | string\n /** Custom function to filter options based on search term */\n filterOptions?: (options: Array<T>, searchTerm: string) => Array<T>\n /** Callback function executed when the clear button is clicked. When provided, an X button will appear on hover to clear the selection. */\n onClear?: boolean | (() => void)\n /** Whether the selection can be cleared */\n clearable?: boolean\n /** Whether the combobox is inside a modal */\n modal?: boolean\n}\n\nexport type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {\n multiple?: false\n value?: string\n onChange: (value: string) => void\n /** Custom render function for the selected value display. */\n renderLabel?: (selectedOption: T) => React.ReactNode\n}\n\nexport type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {\n multiple: true\n value?: Array<string>\n onChange: (value: Array<string>) => void\n /** Custom render function for the selected value(s) display. */\n renderLabel?: (selectedOptions: Array<T>, onRemove: (value: string) => void) => React.ReactNode\n}\n\nexport type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>\n\n/**\n * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.\n *\n * @example\n * ```tsx\n * interface User {\n * id: string\n * name: string\n * email: string\n * }\n *\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * />\n * ```\n *\n * @example Custom filtering\n * ```tsx\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * filterOptions={(options, searchTerm) =>\n * options.filter(user =>\n * user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n * user.email.toLowerCase().includes(searchTerm.toLowerCase())\n * )\n * }\n * />\n * ```\n */\nexport const Combobox = <T extends object>(props: ComboboxProps<T>) => {\n const {\n id,\n options,\n getOptionValue,\n getOptionLabel,\n placeholder = 'Select...',\n searchPlaceholder = 'Search...',\n emptyMessage = 'No results found.',\n className = 'w-60',\n disabled = false,\n maxHeight = '300px',\n renderOption,\n loading = false,\n loadingPlaceholder = 'Cargando...',\n error = false,\n filterOptions,\n multiple,\n clearable = true,\n onClear,\n modal = false,\n } = props\n\n const [open, setOpen] = useState(false)\n const [searchTerm, setSearchTerm] = useState('')\n const [isHovered, setIsHovered] = useState(false)\n\n const closeOnSelect = props.closeOnSelect ?? !props.multiple\n\n const filteredOptions = useMemo(() => {\n if (!filterOptions) {\n return options.filter((option) => getOptionLabel(option).toLowerCase().includes(searchTerm.toLowerCase()))\n }\n\n return searchTerm ? filterOptions(options, searchTerm) : options\n }, [filterOptions, options, searchTerm, getOptionLabel])\n\n const handleSelect = (currentValue: string) => {\n if (multiple) {\n const { value = [], onChange } = props\n const isRemoving = value.includes(currentValue)\n\n // If trying to remove the last item and clearable is false, don't allow it\n if (isRemoving && !clearable && value.length === 1) {\n return\n }\n\n const newValues = isRemoving ? value.filter((val) => val !== currentValue) : [...value, currentValue]\n onChange(newValues)\n } else {\n const { value, onChange } = props\n const newValue = clearable && currentValue === value ? '' : currentValue\n onChange(newValue)\n }\n\n if (closeOnSelect) {\n setOpen(false)\n }\n }\n\n const handleRemove = (valueToRemove: string) => {\n if (props.multiple) {\n const { value = [], onChange } = props\n\n // If trying to remove the last item and clearable is false, don't allow it\n if (!clearable && value.length === 1) {\n return\n }\n\n const newValues = value.filter((val) => val !== valueToRemove)\n onChange(newValues)\n }\n }\n\n const handleClear = (event: React.MouseEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (multiple) {\n const { onChange } = props\n\n // Only clear if clearable is true\n if (clearable) {\n onChange([])\n }\n } else {\n const { onChange } = props\n onChange('')\n }\n\n if (typeof onClear === 'function') {\n onClear()\n }\n }\n\n const displayValue = useMemo(() => {\n if (multiple) {\n const { value = [], renderLabel } = props\n if (value.length === 0) return placeholder\n\n const selectedOptions = options.filter((option) => value.includes(getOptionValue(option)))\n\n if (renderLabel) {\n return renderLabel(selectedOptions, handleRemove)\n }\n\n return selectedOptions.map(getOptionLabel).join(', ')\n }\n\n const { value, renderLabel } = props\n const selectedOption = options.find((option) => getOptionValue(option) === value)\n if (renderLabel && selectedOption) {\n return renderLabel(selectedOption)\n }\n return selectedOption ? getOptionLabel(selectedOption) : placeholder\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props, getOptionLabel, getOptionValue, options, placeholder, multiple])\n\n const hasValue = useMemo(() => {\n if (multiple) {\n return props.value && props.value.length > 0\n }\n return !!props.value\n }, [props.value, multiple])\n\n const showClearButton = onClear && hasValue && isHovered && clearable\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <PopoverRoot open={open} onOpenChange={setOpen} modal={modal}>\n <PopoverTrigger asChild>\n <Button\n id={id}\n type=\"button\"\n disabled={disabled}\n variant=\"text\"\n className={cn(\n buttonVariants({ variant: 'input' }),\n 'relative flex w-full justify-between rounded-lg',\n multiple && 'renderLabel' in props && props.renderLabel ? 'h-auto min-h-10' : 'h-10',\n open && 'border-neutral-950',\n disabled && 'cursor-not-allowed',\n error && 'border-error-400 focus-visible:border-error-700',\n className,\n )}\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <span\n className={cn(\n 'block',\n !hasValue && 'text-neutral-300',\n !(multiple && 'renderLabel' in props && props.renderLabel) && 'truncate',\n )}\n >\n {displayValue}\n </span>\n <CaretUpDown\n className={cn(\n 'h-4 w-4 shrink-0 opacity-50 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-50',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 z-10 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"p-0\">\n <Command shouldFilter={false}>\n <CommandInput\n placeholder={searchPlaceholder}\n disabled={loading}\n value={searchTerm}\n onValueChange={setSearchTerm}\n />\n <CommandList style={{ maxHeight }}>\n {loading ? (\n <div className=\"text-muted-foreground flex items-center justify-center py-6 text-sm\">\n {loadingPlaceholder}\n </div>\n ) : (\n <>\n {filteredOptions.length === 0 && <CommandEmpty>{emptyMessage}</CommandEmpty>}\n <CommandGroup>\n {filteredOptions.map((option) => {\n const optionValue = getOptionValue(option)\n const isSelected = multiple ? props.value?.includes(optionValue) : props.value === optionValue\n\n return (\n <CommandItem key={optionValue} value={optionValue} onSelect={handleSelect}>\n {renderOption ? (\n renderOption(option, isSelected ?? false)\n ) : (\n <>\n <Check className={`mr-2 h-4 w-4 ${isSelected ? 'opacity-100' : 'opacity-0'}`} />\n <ComboboxOptionWithTooltip label={getOptionLabel(option)} />\n </>\n )}\n </CommandItem>\n )\n })}\n </CommandGroup>\n </>\n )}\n </CommandList>\n </Command>\n </PopoverContent>\n </PopoverRoot>\n {typeof error === 'string' && <span className=\"text-error-500 text-sm\">{error}</span>}\n </div>\n )\n}\n"],"names":["value","renderLabel"],"mappings":"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"}
|
|
@@ -82,8 +82,12 @@ export type DatePickerProps<V extends 'single' | 'range' = 'single', F extends V
|
|
|
82
82
|
* When provided, an X button will appear on hover to clear the selected date.
|
|
83
83
|
*/
|
|
84
84
|
readonly onClear?: boolean | (() => void);
|
|
85
|
+
/**
|
|
86
|
+
* Weather the picker is inside a modal (needed for pointer events to work at mobile)
|
|
87
|
+
*/
|
|
88
|
+
modal?: boolean;
|
|
85
89
|
} & Omit<PropsBase, 'mode' | 'selected' | 'onSelect' | 'locale'>;
|
|
86
|
-
declare function DatePicker<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'>({ variant, placeholder, valueFormat, initialValue: initialValueProp, value: valueProp, onChange, buttonClassName, displayFormat, closeOnSelect, showYearSwitcher, locale: localeProp, defaultMonth, onClear, ...rest }: DatePickerProps<V, F>): import("react/jsx-runtime").JSX.Element;
|
|
90
|
+
declare function DatePicker<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'>({ variant, placeholder, valueFormat, initialValue: initialValueProp, value: valueProp, onChange, buttonClassName, displayFormat, closeOnSelect, showYearSwitcher, locale: localeProp, defaultMonth, onClear, modal, ...rest }: DatePickerProps<V, F>): import("react/jsx-runtime").JSX.Element;
|
|
87
91
|
declare namespace DatePicker {
|
|
88
92
|
var displayName: string;
|
|
89
93
|
}
|
|
@@ -8,6 +8,7 @@ import { Button, buttonVariants } from '../Button/Button.js';
|
|
|
8
8
|
import { Calendar } from '../Calendar/Calendar.js';
|
|
9
9
|
import { PopoverRoot, PopoverTrigger, PopoverContent } from '../Popover/Popover.js';
|
|
10
10
|
import { parseInputDate, parseInputRange, formatOutputDate, formatOutputRange } from '../../lib/dateUtils.js';
|
|
11
|
+
import { useIsMobile } from '../../lib/useMobile.js';
|
|
11
12
|
import { cn } from '../../lib/utils.js';
|
|
12
13
|
|
|
13
14
|
const localeMap = {
|
|
@@ -33,10 +34,13 @@ function DatePicker({
|
|
|
33
34
|
locale: localeProp = "enUS",
|
|
34
35
|
defaultMonth,
|
|
35
36
|
onClear,
|
|
37
|
+
modal = false,
|
|
36
38
|
...rest
|
|
37
39
|
}) {
|
|
38
40
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
39
41
|
const [isHovered, setIsHovered] = React.useState(false);
|
|
42
|
+
const isMobile = useIsMobile();
|
|
43
|
+
const calendarRef = React.useRef(null);
|
|
40
44
|
const resolvedLocale = React.useMemo(() => {
|
|
41
45
|
if (typeof localeProp === "string") {
|
|
42
46
|
return localeMap[localeProp];
|
|
@@ -64,6 +68,15 @@ function DatePicker({
|
|
|
64
68
|
setInternalDateRange(newRange);
|
|
65
69
|
}
|
|
66
70
|
}, [valueProp, variant]);
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
if (isOpen && isMobile && calendarRef.current) {
|
|
73
|
+
const timeoutId = setTimeout(() => {
|
|
74
|
+
const calendarElement = calendarRef.current?.querySelector('[id="calendar"]');
|
|
75
|
+
calendarElement?.focus();
|
|
76
|
+
}, 100);
|
|
77
|
+
return () => clearTimeout(timeoutId);
|
|
78
|
+
}
|
|
79
|
+
}, [isOpen, isMobile]);
|
|
67
80
|
const singleDate = internalSingleDate;
|
|
68
81
|
const dateRange = internalDateRange;
|
|
69
82
|
const getEmptyValue = React.useCallback(() => {
|
|
@@ -106,6 +119,7 @@ function DatePicker({
|
|
|
106
119
|
);
|
|
107
120
|
const handleClear = React.useCallback(
|
|
108
121
|
(event) => {
|
|
122
|
+
if (isMobile) return;
|
|
109
123
|
event.preventDefault();
|
|
110
124
|
event.stopPropagation();
|
|
111
125
|
if (variant === "single") {
|
|
@@ -120,7 +134,7 @@ function DatePicker({
|
|
|
120
134
|
onClear();
|
|
121
135
|
}
|
|
122
136
|
},
|
|
123
|
-
[variant, onChange, onClear, getEmptyValue]
|
|
137
|
+
[variant, onChange, onClear, getEmptyValue, isMobile]
|
|
124
138
|
);
|
|
125
139
|
const formatForDisplay = () => {
|
|
126
140
|
const formatOptions = { locale: resolvedLocale };
|
|
@@ -160,7 +174,7 @@ function DatePicker({
|
|
|
160
174
|
}, [variant, rest, singleDate, dateRange, handleSelect, resolvedLocale, defaultMonth]);
|
|
161
175
|
const hasValue = variant === "single" ? singleDate : dateRange;
|
|
162
176
|
const showClearButton = onClear && hasValue && isHovered;
|
|
163
|
-
return /* @__PURE__ */ jsxs(PopoverRoot, { open: isOpen, onOpenChange: setIsOpen, children: [
|
|
177
|
+
return /* @__PURE__ */ jsxs(PopoverRoot, { open: isOpen, onOpenChange: setIsOpen, modal, children: [
|
|
164
178
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
165
179
|
Button,
|
|
166
180
|
{
|
|
@@ -173,8 +187,8 @@ function DatePicker({
|
|
|
173
187
|
buttonClassName
|
|
174
188
|
),
|
|
175
189
|
disabled: typeof rest.disabled === "boolean" ? rest.disabled : false,
|
|
176
|
-
onMouseEnter: () => setIsHovered(true),
|
|
177
|
-
onMouseLeave: () => setIsHovered(false),
|
|
190
|
+
onMouseEnter: () => !isMobile && setIsHovered(true),
|
|
191
|
+
onMouseLeave: () => !isMobile && setIsHovered(false),
|
|
178
192
|
children: [
|
|
179
193
|
/* @__PURE__ */ jsx("span", { className: "w-full pr-7 text-center", children: formatForDisplay() }),
|
|
180
194
|
/* @__PURE__ */ jsx(
|
|
@@ -200,7 +214,7 @@ function DatePicker({
|
|
|
200
214
|
]
|
|
201
215
|
}
|
|
202
216
|
) }),
|
|
203
|
-
/* @__PURE__ */ jsx(PopoverContent, { className: "w-auto p-0", align: "center", children: /* @__PURE__ */ jsx(
|
|
217
|
+
/* @__PURE__ */ jsx(PopoverContent, { className: "w-auto p-0", align: "center", ref: calendarRef, children: /* @__PURE__ */ jsx(
|
|
204
218
|
Calendar,
|
|
205
219
|
{
|
|
206
220
|
...calendarProps,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatePicker.js","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport { CalendarBlank, X } from '@phosphor-icons/react'\nimport { format as formatFn, Locale } from 'date-fns'\nimport { enUS, es, pt, enGB, de, it, fr } from 'date-fns/locale'\nimport * as React from 'react'\nimport { PropsBase, PropsRangeRequired, PropsSingleRequired } from 'react-day-picker'\n\nimport { Button, buttonVariants } from '../Button'\nimport { Calendar } from '../Calendar'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\n\nimport {\n formatOutputDate,\n formatOutputRange,\n parseInputDate,\n parseInputRange,\n InputDate,\n InputRange,\n ValueFormat,\n} from '@/lib/dateUtils'\nimport { cn } from '@/lib/utils'\n\ntype SupportedLocaleString = 'enUS' | 'es' | 'pt' | 'enGB' | 'de' | 'it' | 'fr'\n\nconst localeMap: Record<SupportedLocaleString, Locale> = {\n enUS,\n es,\n pt,\n enGB,\n de,\n it,\n fr,\n}\n\nexport type DateRange = {\n from?: Date\n to?: Date\n}\n\nexport type IsoDateRange = {\n from?: string\n to?: string\n}\n\ntype DatePickerOutput<Variant extends 'single' | 'range', Format extends ValueFormat> = Variant extends 'single'\n ? Format extends 'date'\n ? Date | null\n : string | null\n : Format extends 'date'\n ? DateRange | null\n : IsoDateRange | null\n\ntype DatePickerInput<V extends 'single' | 'range'> = V extends 'single'\n ? Date | string | null\n : { from?: Date | string; to?: Date | string } | null\n\nexport type DatePickerProps<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'> = {\n /**\n * Placeholder text displayed when no date is selected\n * @default 'Pick a date' for single mode, 'Pick a date range' for range mode\n */\n readonly placeholder?: string\n\n /**\n * Format string to use when displaying the selected date in the button\n * @default 'MMM d, yyyy'\n */\n readonly displayFormat?: string\n\n /**\n * ClassName for the button\n */\n readonly buttonClassName?: string\n\n /**\n * Whether to allow the user to show the year switcher menu\n * @default true for single mode, false for range mode\n */\n readonly showYearSwitcher?: boolean\n\n /**\n * Initial value for the date picker\n * Accepts both Date objects and ISO strings regardless of valueFormat setting\n */\n readonly initialValue?: DatePickerInput<V>\n\n /**\n * Current value for the date picker\n * Accepts both Date objects and ISO strings regardless of valueFormat setting\n */\n readonly value?: DatePickerInput<V>\n\n /**\n * Determines if the picker should close after a selection\n * @default true for single mode, false for range mode\n */\n readonly closeOnSelect?: boolean\n\n /**\n * Determines the format of the value provided to the onChange callback\n * - 'iso' (default): onChange receives ISO string(s) ('yyyy-MM-dd') or empty string for cleared values\n * - 'date': onChange receives JavaScript Date object(s) or null for cleared values\n *\n * Note: The component accepts both Date objects and ISO strings for value/initialValue\n * regardless of this setting. Empty values are sent as null (date format) or '' (iso format)\n * instead of undefined to work properly with React Hook Form.\n * @default 'iso'\n */\n readonly valueFormat?: F\n\n /**\n * Callback when date or date range changes\n */\n readonly onChange?: (value: DatePickerOutput<V, F>) => void\n\n /**\n * DatePicker mode - single date or date range\n * @default 'single'\n */\n readonly variant?: V\n\n /**\n * The locale to use for formatting dates and determining the start of the week.\n * Can be a string identifier for supported locales ('enUS', 'es', 'pt', 'enGB', 'de', 'it', 'fr')\n * or a Locale object from date-fns/locale for other languages.\n * @default 'enUS'\n */\n readonly locale?: SupportedLocaleString | Locale\n\n /**\n * Callback function executed when the clear button is clicked.\n * When provided, an X button will appear on hover to clear the selected date.\n */\n readonly onClear?: boolean | (() => void)\n} & Omit<PropsBase, 'mode' | 'selected' | 'onSelect' | 'locale'>\n\nfunction DatePicker<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'>({\n variant = 'single' as V,\n placeholder = variant === 'single' ? 'Pick a date' : 'Pick a date range',\n valueFormat = 'iso' as F,\n initialValue: initialValueProp,\n value: valueProp,\n onChange,\n buttonClassName,\n displayFormat = 'MMM d, yyyy',\n closeOnSelect = variant === 'single',\n showYearSwitcher = variant === 'single',\n locale: localeProp = 'enUS',\n defaultMonth,\n onClear,\n ...rest\n}: DatePickerProps<V, F>) {\n const [isOpen, setIsOpen] = React.useState(false)\n const [isHovered, setIsHovered] = React.useState(false)\n\n const resolvedLocale = React.useMemo(() => {\n if (typeof localeProp === 'string') {\n return localeMap[localeProp]\n }\n return localeProp\n }, [localeProp])\n\n const [internalSingleDate, setInternalSingleDate] = React.useState<Date | undefined>(() => {\n if (variant === 'single') {\n return parseInputDate(initialValueProp as InputDate)\n }\n return undefined\n })\n\n const [internalDateRange, setInternalDateRange] = React.useState<DateRange | undefined>(() => {\n if (variant === 'range') {\n return parseInputRange(initialValueProp as InputRange)\n }\n return undefined\n })\n\n React.useEffect(() => {\n if (variant === 'single') {\n const newDate = parseInputDate(valueProp as InputDate)\n setInternalSingleDate(newDate)\n } else {\n const newRange = parseInputRange(valueProp as InputRange)\n setInternalDateRange(newRange)\n }\n }, [valueProp, variant])\n\n const singleDate = internalSingleDate\n const dateRange = internalDateRange\n\n const getEmptyValue = React.useCallback((): DatePickerOutput<V, F> => {\n if (variant === 'single') {\n return (valueFormat === 'date' ? null : '') as DatePickerOutput<V, F>\n } else {\n return (valueFormat === 'date' ? null : { from: '', to: '' }) as DatePickerOutput<V, F>\n }\n }, [variant, valueFormat])\n\n const handleSelect = React.useCallback(\n (selectedDate: Date | DateRange | undefined) => {\n if (variant === 'single') {\n const date = selectedDate as Date | undefined\n\n setInternalSingleDate(date)\n\n if (onChange) {\n if (date) {\n const output = valueFormat === 'date' ? date : formatOutputDate(date, 'iso')\n onChange(output as DatePickerOutput<V, F>)\n } else {\n onChange(getEmptyValue())\n }\n }\n\n if (closeOnSelect) {\n setIsOpen(false)\n }\n } else {\n const range = selectedDate as DateRange | undefined\n setInternalDateRange(range)\n if (onChange) {\n if (range) {\n const output = valueFormat === 'date' ? range : formatOutputRange(range, 'iso')\n onChange(output as DatePickerOutput<V, F>)\n } else {\n onChange(getEmptyValue())\n }\n }\n }\n },\n [variant, valueFormat, onChange, closeOnSelect, getEmptyValue],\n )\n\n const handleClear = React.useCallback(\n (event: React.MouseEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (variant === 'single') {\n setInternalSingleDate(undefined)\n } else {\n setInternalDateRange(undefined)\n }\n\n if (onChange) {\n onChange(getEmptyValue())\n }\n\n if (typeof onClear === 'function') {\n onClear()\n }\n },\n [variant, onChange, onClear, getEmptyValue],\n )\n\n const formatForDisplay = () => {\n const formatOptions = { locale: resolvedLocale }\n if (variant === 'single') {\n return singleDate ? formatFn(singleDate, displayFormat, formatOptions) : placeholder\n }\n\n if (!dateRange) return placeholder\n const fromStr = dateRange.from ? formatFn(dateRange.from, displayFormat, formatOptions) : '...'\n const toStr = dateRange.to ? formatFn(dateRange.to, displayFormat, formatOptions) : '...'\n if (!dateRange.from && !dateRange.to) return placeholder\n if (!dateRange.from) return `... - ${toStr}`\n if (!dateRange.to) return `${fromStr} - ...`\n return `${fromStr} - ${toStr}`\n }\n\n const calendarProps = React.useMemo(() => {\n const baseProps = {\n defaultMonth: (variant === 'single' ? singleDate : dateRange?.from) ?? defaultMonth,\n locale: resolvedLocale,\n initialFocus: true,\n ...rest,\n } as PropsBase\n\n if (variant === 'single') {\n return {\n ...baseProps,\n mode: 'single' as const,\n selected: singleDate,\n onSelect: (date: Date | undefined) => handleSelect(date),\n } as PropsSingleRequired\n }\n\n return {\n ...baseProps,\n mode: 'range' as const,\n selected: dateRange,\n onSelect: (range: DateRange | undefined) => handleSelect(range),\n numberOfMonths: rest.numberOfMonths ?? 2,\n } as PropsRangeRequired\n }, [variant, rest, singleDate, dateRange, handleSelect, resolvedLocale, defaultMonth])\n\n const hasValue = variant === 'single' ? singleDate : dateRange\n const showClearButton = onClear && hasValue && isHovered\n\n return (\n <PopoverRoot open={isOpen} onOpenChange={setIsOpen}>\n <PopoverTrigger asChild>\n <Button\n id={rest.id}\n variant=\"ghost\"\n className={cn(\n 'relative flex w-fit items-center justify-start text-left font-normal',\n !hasValue && 'text-muted-foreground',\n buttonVariants({ variant: 'input' }),\n buttonClassName,\n )}\n disabled={typeof rest.disabled === 'boolean' ? rest.disabled : false}\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <span className=\"w-full pr-7 text-center\">{formatForDisplay()}</span>\n <CalendarBlank\n className={cn(\n 'absolute right-4 h-4 w-4 shrink-0 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-100',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-0\" align=\"center\">\n <Calendar\n {...calendarProps}\n className=\"border-0\"\n showYearSwitcher={variant === 'single' ? showYearSwitcher : false}\n />\n </PopoverContent>\n </PopoverRoot>\n )\n}\n\nDatePicker.displayName = 'DatePicker'\n\nexport { DatePicker }\n"],"names":[],"mappings":";;;;;;;;;;;;AAyBA;AAAyD;AACvD;AACA;AACA;AACA;AACA;AACA;AAEF;AAwGA;AAA4F;AAChF;AAC2C;AACvC;AACA;AACP;AACP;AACA;AACgB;AACY;AACG;AACV;AACrB;AACA;AAEF;AACE;AACA;AAEA;AACE;AACE;AAA2B;AAE7B;AAAO;AAGT;AACE;AACE;AAAmD;AAErD;AAAO;AAGT;AACE;AACE;AAAqD;AAEvD;AAAO;AAGT;AACE;AACE;AACA;AAA6B;AAE7B;AACA;AAA6B;AAC/B;AAGF;AACA;AAEA;AACE;AACE;AAAwC;AAExC;AAA2D;AAC7D;AAGF;AAA2B;AAEvB;AACE;AAEA;AAEA;AACE;AACE;AACA;AAAyC;AAEzC;AAAwB;AAC1B;AAGF;AACE;AAAe;AACjB;AAEA;AACA;AACA;AACE;AACE;AACA;AAAyC;AAEzC;AAAwB;AAC1B;AACF;AACF;AACF;AAC6D;AAG/D;AAA0B;AAEtB;AACA;AAEA;AACE;AAA+B;AAE/B;AAA8B;AAGhC;AACE;AAAwB;AAG1B;AACE;AAAQ;AACV;AACF;AAC0C;AAG5C;AACE;AACA;AACE;AAAyE;AAG3E;AACA;AACA;AACA;AACA;AACA;AACA;AAA4B;AAG9B;AACE;AAAkB;AACuD;AAC/D;AACM;AACX;AAGL;AACE;AAAO;AACF;AACG;AACI;AAC6C;AACzD;AAGF;AAAO;AACF;AACG;AACI;AACoD;AACvB;AACzC;AAGF;AACA;AAEA;AAEI;AACE;AAAC;AAAA;AACU;AACD;AACG;AACT;AACa;AACsB;AACnC;AACF;AAC+D;AAC1B;AACC;AAEtC;AAA8D;AAC9D;AAAC;AAAA;AACY;AACT;AACgC;AAClC;AAAA;AACF;AAEE;AAAC;AAAA;AACa;AACD;AACT;AACmD;AACrD;AACS;AAAA;AACX;AAAA;AAAA;AAGN;AAEE;AAAC;AAAA;AACK;AACM;AACkD;AAAA;AAEhE;AAGN;AAEA;;"}
|
|
1
|
+
{"version":3,"file":"DatePicker.js","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport { CalendarBlank, X } from '@phosphor-icons/react'\nimport { format as formatFn, Locale } from 'date-fns'\nimport { enUS, es, pt, enGB, de, it, fr } from 'date-fns/locale'\nimport * as React from 'react'\nimport { PropsBase, PropsRangeRequired, PropsSingleRequired } from 'react-day-picker'\n\nimport { Button, buttonVariants } from '../Button'\nimport { Calendar } from '../Calendar'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\n\nimport {\n formatOutputDate,\n formatOutputRange,\n parseInputDate,\n parseInputRange,\n InputDate,\n InputRange,\n ValueFormat,\n} from '@/lib/dateUtils'\nimport { useIsMobile } from '@/lib/useMobile'\nimport { cn } from '@/lib/utils'\n\ntype SupportedLocaleString = 'enUS' | 'es' | 'pt' | 'enGB' | 'de' | 'it' | 'fr'\n\nconst localeMap: Record<SupportedLocaleString, Locale> = {\n enUS,\n es,\n pt,\n enGB,\n de,\n it,\n fr,\n}\n\nexport type DateRange = {\n from?: Date\n to?: Date\n}\n\nexport type IsoDateRange = {\n from?: string\n to?: string\n}\n\ntype DatePickerOutput<Variant extends 'single' | 'range', Format extends ValueFormat> = Variant extends 'single'\n ? Format extends 'date'\n ? Date | null\n : string | null\n : Format extends 'date'\n ? DateRange | null\n : IsoDateRange | null\n\ntype DatePickerInput<V extends 'single' | 'range'> = V extends 'single'\n ? Date | string | null\n : { from?: Date | string; to?: Date | string } | null\n\nexport type DatePickerProps<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'> = {\n /**\n * Placeholder text displayed when no date is selected\n * @default 'Pick a date' for single mode, 'Pick a date range' for range mode\n */\n readonly placeholder?: string\n\n /**\n * Format string to use when displaying the selected date in the button\n * @default 'MMM d, yyyy'\n */\n readonly displayFormat?: string\n\n /**\n * ClassName for the button\n */\n readonly buttonClassName?: string\n\n /**\n * Whether to allow the user to show the year switcher menu\n * @default true for single mode, false for range mode\n */\n readonly showYearSwitcher?: boolean\n\n /**\n * Initial value for the date picker\n * Accepts both Date objects and ISO strings regardless of valueFormat setting\n */\n readonly initialValue?: DatePickerInput<V>\n\n /**\n * Current value for the date picker\n * Accepts both Date objects and ISO strings regardless of valueFormat setting\n */\n readonly value?: DatePickerInput<V>\n\n /**\n * Determines if the picker should close after a selection\n * @default true for single mode, false for range mode\n */\n readonly closeOnSelect?: boolean\n\n /**\n * Determines the format of the value provided to the onChange callback\n * - 'iso' (default): onChange receives ISO string(s) ('yyyy-MM-dd') or empty string for cleared values\n * - 'date': onChange receives JavaScript Date object(s) or null for cleared values\n *\n * Note: The component accepts both Date objects and ISO strings for value/initialValue\n * regardless of this setting. Empty values are sent as null (date format) or '' (iso format)\n * instead of undefined to work properly with React Hook Form.\n * @default 'iso'\n */\n readonly valueFormat?: F\n\n /**\n * Callback when date or date range changes\n */\n readonly onChange?: (value: DatePickerOutput<V, F>) => void\n\n /**\n * DatePicker mode - single date or date range\n * @default 'single'\n */\n readonly variant?: V\n\n /**\n * The locale to use for formatting dates and determining the start of the week.\n * Can be a string identifier for supported locales ('enUS', 'es', 'pt', 'enGB', 'de', 'it', 'fr')\n * or a Locale object from date-fns/locale for other languages.\n * @default 'enUS'\n */\n readonly locale?: SupportedLocaleString | Locale\n\n /**\n * Callback function executed when the clear button is clicked.\n * When provided, an X button will appear on hover to clear the selected date.\n */\n readonly onClear?: boolean | (() => void)\n\n /**\n * Weather the picker is inside a modal (needed for pointer events to work at mobile)\n */\n modal?: boolean\n} & Omit<PropsBase, 'mode' | 'selected' | 'onSelect' | 'locale'>\n\nfunction DatePicker<V extends 'single' | 'range' = 'single', F extends ValueFormat = 'iso'>({\n variant = 'single' as V,\n placeholder = variant === 'single' ? 'Pick a date' : 'Pick a date range',\n valueFormat = 'iso' as F,\n initialValue: initialValueProp,\n value: valueProp,\n onChange,\n buttonClassName,\n displayFormat = 'MMM d, yyyy',\n closeOnSelect = variant === 'single',\n showYearSwitcher = variant === 'single',\n locale: localeProp = 'enUS',\n defaultMonth,\n onClear,\n modal = false,\n ...rest\n}: DatePickerProps<V, F>) {\n const [isOpen, setIsOpen] = React.useState(false)\n const [isHovered, setIsHovered] = React.useState(false)\n const isMobile = useIsMobile()\n const calendarRef = React.useRef<HTMLDivElement>(null)\n\n const resolvedLocale = React.useMemo(() => {\n if (typeof localeProp === 'string') {\n return localeMap[localeProp]\n }\n return localeProp\n }, [localeProp])\n\n const [internalSingleDate, setInternalSingleDate] = React.useState<Date | undefined>(() => {\n if (variant === 'single') {\n return parseInputDate(initialValueProp as InputDate)\n }\n return undefined\n })\n\n const [internalDateRange, setInternalDateRange] = React.useState<DateRange | undefined>(() => {\n if (variant === 'range') {\n return parseInputRange(initialValueProp as InputRange)\n }\n return undefined\n })\n\n React.useEffect(() => {\n if (variant === 'single') {\n const newDate = parseInputDate(valueProp as InputDate)\n setInternalSingleDate(newDate)\n } else {\n const newRange = parseInputRange(valueProp as InputRange)\n setInternalDateRange(newRange)\n }\n }, [valueProp, variant])\n\n React.useEffect(() => {\n if (isOpen && isMobile && calendarRef.current) {\n const timeoutId = setTimeout(() => {\n const calendarElement = calendarRef.current?.querySelector('[id=\"calendar\"]') as HTMLElement | null\n\n calendarElement?.focus()\n }, 100)\n\n return () => clearTimeout(timeoutId)\n }\n }, [isOpen, isMobile])\n\n const singleDate = internalSingleDate\n const dateRange = internalDateRange\n\n const getEmptyValue = React.useCallback((): DatePickerOutput<V, F> => {\n if (variant === 'single') {\n return (valueFormat === 'date' ? null : '') as DatePickerOutput<V, F>\n } else {\n return (valueFormat === 'date' ? null : { from: '', to: '' }) as DatePickerOutput<V, F>\n }\n }, [variant, valueFormat])\n\n const handleSelect = React.useCallback(\n (selectedDate: Date | DateRange | undefined) => {\n if (variant === 'single') {\n const date = selectedDate as Date | undefined\n\n setInternalSingleDate(date)\n\n if (onChange) {\n if (date) {\n const output = valueFormat === 'date' ? date : formatOutputDate(date, 'iso')\n onChange(output as DatePickerOutput<V, F>)\n } else {\n onChange(getEmptyValue())\n }\n }\n\n if (closeOnSelect) {\n setIsOpen(false)\n }\n } else {\n const range = selectedDate as DateRange | undefined\n setInternalDateRange(range)\n if (onChange) {\n if (range) {\n const output = valueFormat === 'date' ? range : formatOutputRange(range, 'iso')\n onChange(output as DatePickerOutput<V, F>)\n } else {\n onChange(getEmptyValue())\n }\n }\n }\n },\n [variant, valueFormat, onChange, closeOnSelect, getEmptyValue],\n )\n\n const handleClear = React.useCallback(\n (event: React.MouseEvent) => {\n if (isMobile) return\n\n event.preventDefault()\n event.stopPropagation()\n\n if (variant === 'single') {\n setInternalSingleDate(undefined)\n } else {\n setInternalDateRange(undefined)\n }\n\n if (onChange) {\n onChange(getEmptyValue())\n }\n\n if (typeof onClear === 'function') {\n onClear()\n }\n },\n [variant, onChange, onClear, getEmptyValue, isMobile],\n )\n\n const formatForDisplay = () => {\n const formatOptions = { locale: resolvedLocale }\n if (variant === 'single') {\n return singleDate ? formatFn(singleDate, displayFormat, formatOptions) : placeholder\n }\n\n if (!dateRange) return placeholder\n const fromStr = dateRange.from ? formatFn(dateRange.from, displayFormat, formatOptions) : '...'\n const toStr = dateRange.to ? formatFn(dateRange.to, displayFormat, formatOptions) : '...'\n if (!dateRange.from && !dateRange.to) return placeholder\n if (!dateRange.from) return `... - ${toStr}`\n if (!dateRange.to) return `${fromStr} - ...`\n return `${fromStr} - ${toStr}`\n }\n\n const calendarProps = React.useMemo(() => {\n const baseProps = {\n defaultMonth: (variant === 'single' ? singleDate : dateRange?.from) ?? defaultMonth,\n locale: resolvedLocale,\n initialFocus: true,\n ...rest,\n } as PropsBase\n\n if (variant === 'single') {\n return {\n ...baseProps,\n mode: 'single' as const,\n selected: singleDate,\n onSelect: (date: Date | undefined) => handleSelect(date),\n } as PropsSingleRequired\n }\n\n return {\n ...baseProps,\n mode: 'range' as const,\n selected: dateRange,\n onSelect: (range: DateRange | undefined) => handleSelect(range),\n numberOfMonths: rest.numberOfMonths ?? 2,\n } as PropsRangeRequired\n }, [variant, rest, singleDate, dateRange, handleSelect, resolvedLocale, defaultMonth])\n\n const hasValue = variant === 'single' ? singleDate : dateRange\n const showClearButton = onClear && hasValue && isHovered\n\n return (\n <PopoverRoot open={isOpen} onOpenChange={setIsOpen} modal={modal}>\n <PopoverTrigger asChild>\n <Button\n id={rest.id}\n variant=\"ghost\"\n className={cn(\n 'relative flex w-fit items-center justify-start text-left font-normal',\n !hasValue && 'text-muted-foreground',\n buttonVariants({ variant: 'input' }),\n buttonClassName,\n )}\n disabled={typeof rest.disabled === 'boolean' ? rest.disabled : false}\n onMouseEnter={() => !isMobile && setIsHovered(true)}\n onMouseLeave={() => !isMobile && setIsHovered(false)}\n >\n <span className=\"w-full pr-7 text-center\">{formatForDisplay()}</span>\n <CalendarBlank\n className={cn(\n 'absolute right-4 h-4 w-4 shrink-0 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-100',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"w-auto p-0\" align=\"center\" ref={calendarRef}>\n <Calendar\n {...calendarProps}\n className=\"border-0\"\n showYearSwitcher={variant === 'single' ? showYearSwitcher : false}\n />\n </PopoverContent>\n </PopoverRoot>\n )\n}\n\nDatePicker.displayName = 'DatePicker'\n\nexport { DatePicker }\n"],"names":[],"mappings":";;;;;;;;;;;;;AA0BA;AAAyD;AACvD;AACA;AACA;AACA;AACA;AACA;AAEF;AA6GA;AAA4F;AAChF;AAC2C;AACvC;AACA;AACP;AACP;AACA;AACgB;AACY;AACG;AACV;AACrB;AACA;AACQ;AAEV;AACE;AACA;AACA;AACA;AAEA;AACE;AACE;AAA2B;AAE7B;AAAO;AAGT;AACE;AACE;AAAmD;AAErD;AAAO;AAGT;AACE;AACE;AAAqD;AAEvD;AAAO;AAGT;AACE;AACE;AACA;AAA6B;AAE7B;AACA;AAA6B;AAC/B;AAGF;AACE;AACE;AACE;AAEA;AAAuB;AAGzB;AAAmC;AACrC;AAGF;AACA;AAEA;AACE;AACE;AAAwC;AAExC;AAA2D;AAC7D;AAGF;AAA2B;AAEvB;AACE;AAEA;AAEA;AACE;AACE;AACA;AAAyC;AAEzC;AAAwB;AAC1B;AAGF;AACE;AAAe;AACjB;AAEA;AACA;AACA;AACE;AACE;AACA;AAAyC;AAEzC;AAAwB;AAC1B;AACF;AACF;AACF;AAC6D;AAG/D;AAA0B;AAEtB;AAEA;AACA;AAEA;AACE;AAA+B;AAE/B;AAA8B;AAGhC;AACE;AAAwB;AAG1B;AACE;AAAQ;AACV;AACF;AACoD;AAGtD;AACE;AACA;AACE;AAAyE;AAG3E;AACA;AACA;AACA;AACA;AACA;AACA;AAA4B;AAG9B;AACE;AAAkB;AACuD;AAC/D;AACM;AACX;AAGL;AACE;AAAO;AACF;AACG;AACI;AAC6C;AACzD;AAGF;AAAO;AACF;AACG;AACI;AACoD;AACvB;AACzC;AAGF;AACA;AAEA;AAEI;AACE;AAAC;AAAA;AACU;AACD;AACG;AACT;AACa;AACsB;AACnC;AACF;AAC+D;AACb;AACC;AAEnD;AAA8D;AAC9D;AAAC;AAAA;AACY;AACT;AACgC;AAClC;AAAA;AACF;AAEE;AAAC;AAAA;AACa;AACD;AACT;AACmD;AACrD;AACS;AAAA;AACX;AAAA;AAAA;AAGN;AAEE;AAAC;AAAA;AACK;AACM;AACkD;AAAA;AAEhE;AAGN;AAEA;;"}
|
|
@@ -27,6 +27,12 @@ export type InlineMultiSelectProps = {
|
|
|
27
27
|
max?: number;
|
|
28
28
|
/** Additional CSS classes */
|
|
29
29
|
className?: string;
|
|
30
|
+
/** Whether the component is in a loading state */
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
/** Message to show when in loading state */
|
|
33
|
+
loadingPlaceholder?: string;
|
|
34
|
+
/** Whether the component is in a modal */
|
|
35
|
+
modal?: boolean;
|
|
30
36
|
};
|
|
31
37
|
/**
|
|
32
38
|
* InlineMultiSelect Component
|
|
@@ -18,8 +18,11 @@ const InlineMultiSelect = React.forwardRef(
|
|
|
18
18
|
commandInputPlaceholder,
|
|
19
19
|
max,
|
|
20
20
|
className,
|
|
21
|
-
fieldState
|
|
22
|
-
|
|
21
|
+
fieldState,
|
|
22
|
+
loading = false,
|
|
23
|
+
modal = false,
|
|
24
|
+
loadingPlaceholder = "Cargando..."
|
|
25
|
+
}) => {
|
|
23
26
|
const inputRef = React.useRef(null);
|
|
24
27
|
const containerRef = React.useRef(null);
|
|
25
28
|
const [open, setOpen] = React.useState(false);
|
|
@@ -49,23 +52,23 @@ const InlineMultiSelect = React.forwardRef(
|
|
|
49
52
|
const newValues = itemToRemove ? selectedValues.filter((val) => val !== itemToRemove.value) : selectedValues.slice(0, -1);
|
|
50
53
|
handleSelectionChange(newValues);
|
|
51
54
|
};
|
|
52
|
-
const handleUnselect = (item,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
const handleUnselect = (item, event) => {
|
|
56
|
+
event.stopPropagation();
|
|
57
|
+
event.preventDefault();
|
|
55
58
|
removeItem(item);
|
|
56
59
|
};
|
|
57
|
-
const handleKeyDown = (
|
|
60
|
+
const handleKeyDown = (event) => {
|
|
58
61
|
const input = inputRef.current;
|
|
59
62
|
if (input) {
|
|
60
|
-
if ((
|
|
63
|
+
if ((event.key === "Backspace" || event.key === "Delete") && input.value === "") {
|
|
61
64
|
removeItem();
|
|
62
65
|
}
|
|
63
|
-
if (
|
|
66
|
+
if (event.key === "Escape") {
|
|
64
67
|
input.blur();
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
};
|
|
68
|
-
return /* @__PURE__ */ jsx(PopoverRoot, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(Command, { onKeyDown: handleKeyDown, children: [
|
|
71
|
+
return /* @__PURE__ */ jsx(PopoverRoot, { open, onOpenChange: setOpen, modal, children: /* @__PURE__ */ jsxs(Command, { onKeyDown: handleKeyDown, children: [
|
|
69
72
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
70
73
|
Button,
|
|
71
74
|
{
|
|
@@ -85,10 +88,10 @@ const InlineMultiSelect = React.forwardRef(
|
|
|
85
88
|
{
|
|
86
89
|
role: "button",
|
|
87
90
|
tabIndex: 0,
|
|
88
|
-
onClick: (
|
|
89
|
-
onKeyDown: (
|
|
90
|
-
if (
|
|
91
|
-
handleUnselect(item,
|
|
91
|
+
onClick: (event) => handleUnselect(item, event),
|
|
92
|
+
onKeyDown: (event) => {
|
|
93
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
94
|
+
handleUnselect(item, event);
|
|
92
95
|
}
|
|
93
96
|
},
|
|
94
97
|
className: "hover:bg-muted-foreground/20 flex h-4 w-4 items-center justify-center rounded-full",
|
|
@@ -110,20 +113,21 @@ const InlineMultiSelect = React.forwardRef(
|
|
|
110
113
|
CommandInput,
|
|
111
114
|
{
|
|
112
115
|
placeholder: commandInputPlaceholder ?? "Search...",
|
|
116
|
+
disabled: loading,
|
|
113
117
|
value: inputValue,
|
|
114
118
|
onValueChange: setInputValue
|
|
115
119
|
}
|
|
116
120
|
),
|
|
117
|
-
/* @__PURE__ */
|
|
121
|
+
/* @__PURE__ */ jsx(CommandList, { children: loading ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex items-center justify-center py-6 text-sm", children: loadingPlaceholder }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
118
122
|
/* @__PURE__ */ jsx(CommandEmpty, { children: notFoundText ?? "Options not found" }),
|
|
119
123
|
open && /* @__PURE__ */ jsx(CommandGroup, { children: options.map((item) => {
|
|
120
124
|
const isSelected = selectedValues.includes(item.value);
|
|
121
125
|
return /* @__PURE__ */ jsx(
|
|
122
126
|
CommandItem,
|
|
123
127
|
{
|
|
124
|
-
onMouseDown: (
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
onMouseDown: (event) => {
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
event.stopPropagation();
|
|
127
131
|
},
|
|
128
132
|
onSelect: () => {
|
|
129
133
|
if (isSelected) {
|
|
@@ -142,7 +146,7 @@ const InlineMultiSelect = React.forwardRef(
|
|
|
142
146
|
item.value
|
|
143
147
|
);
|
|
144
148
|
}) })
|
|
145
|
-
] })
|
|
149
|
+
] }) })
|
|
146
150
|
] })
|
|
147
151
|
] }) });
|
|
148
152
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InlineMultiSelect.js","sources":["../../../src/components/InlineMultiSelect/InlineMultiSelect.tsx"],"sourcesContent":["'use client'\n\nimport { CaretUpDown, Check, X } from '@phosphor-icons/react'\nimport * as React from 'react'\nimport { ControllerFieldState } from 'react-hook-form'\n\nimport { Button } from '../Button'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { PopoverRoot, PopoverContent, PopoverTrigger } from '../Popover'\n\nimport { cn } from '@/lib/utils'\n\n/** Represents a selectable option in the InlineMultiSelect */\ntype Option = { value: string; label: string }\n\n/** Props for the InlineMultiSelect component */\nexport type InlineMultiSelectProps = {\n /** Array of options to display in the select */\n options: Array<Option>\n /** Currently selected values (controlled) */\n value?: Array<string>\n /** Callback fired when selection changes */\n onChange?: (value: Array<string>) => void\n /** Form field state from react-hook-form */\n fieldState?: ControllerFieldState\n /** Default selected values (uncontrolled) */\n defaultValue?: Array<string>\n /** Placeholder text when no items are selected */\n placeholder?: string\n /** Text to show when no options match the search */\n notFoundText?: string\n /** Placeholder for the search input */\n commandInputPlaceholder?: string\n /** Maximum number of items that can be selected */\n max?: number\n /** Additional CSS classes */\n className?: string\n}\n\n/**\n * InlineMultiSelect Component\n *\n * A controlled multi-select component that displays selected items inline with search functionality.\n * Supports keyboard navigation, maximum selection limit, and custom styling.\n */\nexport const InlineMultiSelect = React.forwardRef<HTMLInputElement, InlineMultiSelectProps>(\n (\n
|
|
1
|
+
{"version":3,"file":"InlineMultiSelect.js","sources":["../../../src/components/InlineMultiSelect/InlineMultiSelect.tsx"],"sourcesContent":["'use client'\n\nimport { CaretUpDown, Check, X } from '@phosphor-icons/react'\nimport * as React from 'react'\nimport { ControllerFieldState } from 'react-hook-form'\n\nimport { Button } from '../Button'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { PopoverRoot, PopoverContent, PopoverTrigger } from '../Popover'\n\nimport { cn } from '@/lib/utils'\n\n/** Represents a selectable option in the InlineMultiSelect */\ntype Option = { value: string; label: string }\n\n/** Props for the InlineMultiSelect component */\nexport type InlineMultiSelectProps = {\n /** Array of options to display in the select */\n options: Array<Option>\n /** Currently selected values (controlled) */\n value?: Array<string>\n /** Callback fired when selection changes */\n onChange?: (value: Array<string>) => void\n /** Form field state from react-hook-form */\n fieldState?: ControllerFieldState\n /** Default selected values (uncontrolled) */\n defaultValue?: Array<string>\n /** Placeholder text when no items are selected */\n placeholder?: string\n /** Text to show when no options match the search */\n notFoundText?: string\n /** Placeholder for the search input */\n commandInputPlaceholder?: string\n /** Maximum number of items that can be selected */\n max?: number\n /** Additional CSS classes */\n className?: string\n /** Whether the component is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the component is in a modal */\n modal?: boolean\n}\n\n/**\n * InlineMultiSelect Component\n *\n * A controlled multi-select component that displays selected items inline with search functionality.\n * Supports keyboard navigation, maximum selection limit, and custom styling.\n */\nexport const InlineMultiSelect = React.forwardRef<HTMLInputElement, InlineMultiSelectProps>(\n ({\n options,\n value,\n onChange,\n defaultValue = [],\n placeholder,\n notFoundText,\n commandInputPlaceholder,\n max,\n className,\n fieldState,\n loading = false,\n modal = false,\n loadingPlaceholder = 'Cargando...',\n }) => {\n const inputRef = React.useRef<HTMLInputElement>(null)\n const containerRef = React.useRef<HTMLDivElement>(null)\n const [open, setOpen] = React.useState(false)\n const [inputValue, setInputValue] = React.useState('')\n\n const isControlled = value !== undefined\n\n const [internalSelected, setInternalSelected] = React.useState<Array<string>>(() => defaultValue)\n\n const selectedValues = isControlled ? value! : internalSelected\n const selected = React.useMemo(\n () => options.filter((option) => selectedValues.includes(option.value)),\n [options, selectedValues],\n )\n\n const [visibleItems, setVisibleItems] = React.useState<Array<Option>>([])\n const [hiddenCount, setHiddenCount] = React.useState(0)\n\n React.useEffect(() => {\n const maxVisibleItems = 2\n const visibleCount = Math.min(selected.length, maxVisibleItems)\n setVisibleItems(selected.slice(0, visibleCount))\n setHiddenCount(selected.length - visibleCount)\n }, [selected])\n\n const handleSelectionChange = (newSelected: Array<string>) => {\n if (!isControlled) {\n setInternalSelected(newSelected)\n }\n onChange?.(newSelected)\n }\n\n const removeItem = (itemToRemove?: Option) => {\n const newValues = itemToRemove\n ? selectedValues.filter((val) => val !== itemToRemove.value)\n : selectedValues.slice(0, -1)\n handleSelectionChange(newValues)\n }\n\n const handleUnselect = (item: Option, event: React.MouseEvent | React.KeyboardEvent) => {\n event.stopPropagation()\n event.preventDefault()\n removeItem(item)\n }\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n const input = inputRef.current\n if (input) {\n if ((event.key === 'Backspace' || event.key === 'Delete') && input.value === '') {\n removeItem()\n }\n if (event.key === 'Escape') {\n input.blur()\n }\n }\n }\n\n return (\n <PopoverRoot open={open} onOpenChange={setOpen} modal={modal}>\n <Command onKeyDown={handleKeyDown}>\n <PopoverTrigger asChild>\n <Button\n variant=\"ghost\"\n aria-expanded={open}\n className={cn(\n 'w-full justify-between bg-[#fff] pr-2 pl-2',\n fieldState?.invalid ? 'border-error-400 focus-within:border-error-700' : '',\n className,\n )}\n >\n <div ref={containerRef} className=\"flex flex-1 flex-wrap items-center gap-1 overflow-hidden pr-2\">\n <div className=\"flex items-center whitespace-nowrap\">\n {selected.length > 0 ? (\n <>\n {visibleItems.map((item) => (\n <div key={item.value} className=\"bg-muted flex items-center gap-1 rounded px-1 py-0.5\">\n <span className=\"text-sm\">{item.label}</span>\n <span\n role=\"button\"\n tabIndex={0}\n onClick={(event) => handleUnselect(item, event)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n handleUnselect(item, event)\n }\n }}\n className=\"hover:bg-muted-foreground/20 flex h-4 w-4 items-center justify-center rounded-full\"\n >\n <X className=\"text-muted-foreground h-3 w-3\" />\n </span>\n </div>\n ))}\n {hiddenCount > 0 && <span className=\"text-muted-foreground text-sm\">+{hiddenCount}</span>}\n </>\n ) : (\n <span className=\"text-muted-foreground\">{placeholder}</span>\n )}\n </div>\n </div>\n <CaretUpDown className=\"h-4 w-4 opacity-50\" />\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"px-0 py-0\">\n <CommandInput\n placeholder={commandInputPlaceholder ?? 'Search...'}\n disabled={loading}\n value={inputValue}\n onValueChange={setInputValue}\n />\n <CommandList>\n {loading ? (\n <div className=\"text-muted-foreground flex items-center justify-center py-6 text-sm\">\n {loadingPlaceholder}\n </div>\n ) : (\n <>\n <CommandEmpty>{notFoundText ?? 'Options not found'}</CommandEmpty>\n {open && (\n <CommandGroup>\n {options.map((item) => {\n const isSelected = selectedValues.includes(item.value)\n return (\n <CommandItem\n key={item.value}\n onMouseDown={(event) => {\n event.preventDefault()\n event.stopPropagation()\n }}\n onSelect={() => {\n if (isSelected) {\n handleSelectionChange(selectedValues.filter((val) => val !== item.value))\n } else {\n if (max && selectedValues.length >= max) return\n handleSelectionChange([...selectedValues, item.value])\n setInputValue('')\n }\n }}\n >\n <div className=\"flex items-center\">\n <div className=\"mr-2\">\n {isSelected ? <Check className=\"h-4 w-4\" /> : <div className=\"h-4 w-4\" />}\n </div>\n <div>{item.label}</div>\n </div>\n </CommandItem>\n )\n })}\n </CommandGroup>\n )}\n </>\n )}\n </CommandList>\n </PopoverContent>\n </Command>\n </PopoverRoot>\n )\n },\n)\n\nInlineMultiSelect.displayName = 'InlineMultiSelect'\n"],"names":[],"mappings":";;;;;;;;;AAmDO;AAAgC;AACpC;AACC;AACA;AACA;AACgB;AAChB;AACA;AACA;AACA;AACA;AACA;AACU;AACF;AACa;AAErB;AACA;AACA;AACA;AAEA;AAEA;AAEA;AACA;AAAuB;AACiD;AAC9C;AAG1B;AACA;AAEA;AACE;AACA;AACA;AACA;AAA6C;AAG/C;AACE;AACE;AAA+B;AAEjC;AAAsB;AAGxB;AACE;AAGA;AAA+B;AAGjC;AACE;AACA;AACA;AAAe;AAGjB;AACE;AACA;AACE;AACE;AAAW;AAEb;AACE;AAAW;AACb;AACF;AAGF;AAGM;AACE;AAAC;AAAA;AACS;AACO;AACJ;AACT;AACyE;AACzE;AACF;AAEA;AAIS;AAEG;AAAsC;AACtC;AAAC;AAAA;AACM;AACK;AACoC;AAE5C;AACE;AAA0B;AAC5B;AACF;AACU;AAEmC;AAAA;AAC/C;AAEH;AACmE;AAAA;AAAE;AAAY;AAM1F;AAC4C;AAAA;AAAA;AAEhD;AAEE;AAAA;AAAC;AAAA;AACyC;AAC9B;AACH;AACQ;AAAA;AACjB;AAQM;AAAmD;AAI7C;AACA;AACE;AAAC;AAAA;AAGG;AACA;AAAsB;AACxB;AAEE;AACE;AAAwE;AAExE;AACA;AACA;AAAgB;AAClB;AACF;AAGE;AAEA;AACiB;AACnB;AAAA;AApBU;AAqBZ;AAGN;AAIR;AACF;AAEJ;AAGN;AAEA;;"}
|