feat(ui): add framework-neutral localization support
- add a lightweight UI i18n provider with translation and locale contexts\n- localize accessibility labels across breadcrumb, carousel, dialog, pagination, sheet, sidebar, spinner, and toast components\n- integrate calendar locale data without coupling UI components to Lingui\n- publish tree-shakeable catalogs for eight common locales
This commit is contained in:
@@ -40,6 +40,8 @@
|
|||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./globals.css": "./src/styles/globals.css",
|
"./globals.css": "./src/styles/globals.css",
|
||||||
|
"./locales": "./src/i18n/catalogs.ts",
|
||||||
|
"./locales/*": "./src/i18n/locales/*.ts",
|
||||||
"./lib/*": "./src/lib/*.ts",
|
"./lib/*": "./src/lib/*.ts",
|
||||||
"./components/*": "./src/components/*.tsx",
|
"./components/*": "./src/components/*.tsx",
|
||||||
"./hooks/*": "./src/hooks/*.ts"
|
"./hooks/*": "./src/hooks/*.ts"
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ import { useRender } from "@base-ui/react/use-render"
|
|||||||
|
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
||||||
|
import { LocalizedText, uiMessages, useMessage } from "./i18n"
|
||||||
|
|
||||||
|
function Breadcrumb({
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"nav">) {
|
||||||
|
const defaultLabel = useMessage(uiMessages.breadcrumb)
|
||||||
|
|
||||||
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
aria-label="breadcrumb"
|
aria-label={ariaLabel ?? defaultLabel}
|
||||||
data-slot="breadcrumb"
|
data-slot="breadcrumb"
|
||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -85,9 +92,7 @@ function BreadcrumbSeparator({
|
|||||||
className={cn("[&>svg]:size-3.5", className)}
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? <ChevronRightIcon className="rtl:rotate-180" />}
|
||||||
<ChevronRightIcon className="rtl:rotate-180" />
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -107,9 +112,10 @@ function BreadcrumbEllipsis({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon
|
<MoreHorizontalIcon />
|
||||||
/>
|
<span className="sr-only">
|
||||||
<span className="sr-only">More</span>
|
<LocalizedText message={uiMessages.more} />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import {
|
|||||||
DayPicker,
|
DayPicker,
|
||||||
getDefaultClassNames,
|
getDefaultClassNames,
|
||||||
type DayButton,
|
type DayButton,
|
||||||
type Locale,
|
|
||||||
} from "react-day-picker"
|
} from "react-day-picker"
|
||||||
|
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { Button, buttonVariants } from "@workspace/ui/components/button"
|
import { Button, buttonVariants } from "@workspace/ui/components/button"
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from "lucide-react"
|
import { useCalendarLocale, useLocale } from "@workspace/ui/components/i18n"
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
ChevronDownIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
function Calendar({
|
function Calendar({
|
||||||
className,
|
className,
|
||||||
@@ -18,7 +22,8 @@ function Calendar({
|
|||||||
showOutsideDays = true,
|
showOutsideDays = true,
|
||||||
captionLayout = "label",
|
captionLayout = "label",
|
||||||
buttonVariant = "ghost",
|
buttonVariant = "ghost",
|
||||||
locale,
|
locale: providedLocale,
|
||||||
|
lang,
|
||||||
formatters,
|
formatters,
|
||||||
components,
|
components,
|
||||||
...props
|
...props
|
||||||
@@ -26,6 +31,10 @@ function Calendar({
|
|||||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||||
}) {
|
}) {
|
||||||
const defaultClassNames = getDefaultClassNames()
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
const contextLocale = useLocale()
|
||||||
|
const contextCalendarLocale = useCalendarLocale()
|
||||||
|
const locale = providedLocale ?? contextCalendarLocale
|
||||||
|
const localeCode = locale?.code ?? contextLocale
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DayPicker
|
<DayPicker
|
||||||
@@ -38,9 +47,10 @@ function Calendar({
|
|||||||
)}
|
)}
|
||||||
captionLayout={captionLayout}
|
captionLayout={captionLayout}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
lang={lang ?? localeCode}
|
||||||
formatters={{
|
formatters={{
|
||||||
formatMonthDropdown: (date) =>
|
formatMonthDropdown: (date) =>
|
||||||
date.toLocaleString(locale?.code, { month: "short" }),
|
date.toLocaleString(localeCode, { month: "short" }),
|
||||||
...formatters,
|
...formatters,
|
||||||
}}
|
}}
|
||||||
classNames={{
|
classNames={{
|
||||||
@@ -147,13 +157,19 @@ function Calendar({
|
|||||||
Chevron: ({ className, orientation, ...props }) => {
|
Chevron: ({ className, orientation, ...props }) => {
|
||||||
if (orientation === "left") {
|
if (orientation === "left") {
|
||||||
return (
|
return (
|
||||||
<ChevronLeftIcon className={cn("rtl:rotate-180 size-4", className)} {...props} />
|
<ChevronLeftIcon
|
||||||
|
className={cn("size-4 rtl:rotate-180", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (orientation === "right") {
|
if (orientation === "right") {
|
||||||
return (
|
return (
|
||||||
<ChevronRightIcon className={cn("rtl:rotate-180 size-4", className)} {...props} />
|
<ChevronRightIcon
|
||||||
|
className={cn("size-4 rtl:rotate-180", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +178,7 @@ function Calendar({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
DayButton: ({ ...props }) => (
|
DayButton: ({ ...props }) => (
|
||||||
<CalendarDayButton locale={locale} {...props} />
|
<CalendarDayButton locale={localeCode} {...props} />
|
||||||
),
|
),
|
||||||
WeekNumber: ({ children, ...props }) => {
|
WeekNumber: ({ children, ...props }) => {
|
||||||
return (
|
return (
|
||||||
@@ -186,7 +202,7 @@ function CalendarDayButton({
|
|||||||
modifiers,
|
modifiers,
|
||||||
locale,
|
locale,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
|
}: React.ComponentProps<typeof DayButton> & { locale?: string }) {
|
||||||
const defaultClassNames = getDefaultClassNames()
|
const defaultClassNames = getDefaultClassNames()
|
||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null)
|
const ref = React.useRef<HTMLButtonElement>(null)
|
||||||
@@ -198,7 +214,7 @@ function CalendarDayButton({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
data-day={day.date.toLocaleDateString(locale?.code)}
|
data-day={day.date.toLocaleDateString(locale)}
|
||||||
data-selected-single={
|
data-selected-single={
|
||||||
modifiers.selected &&
|
modifiers.selected &&
|
||||||
!modifiers.range_start &&
|
!modifiers.range_start &&
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import useEmblaCarousel, {
|
|||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
|
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
|
||||||
|
import { LocalizedText, uiMessages } from "./i18n"
|
||||||
|
|
||||||
type CarouselApi = UseEmblaCarouselType[1]
|
type CarouselApi = UseEmblaCarouselType[1]
|
||||||
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
|
||||||
@@ -186,7 +187,7 @@ function CarouselPrevious({
|
|||||||
"absolute touch-manipulation rounded-full",
|
"absolute touch-manipulation rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "inset-y-0 -start-12 my-auto"
|
? "inset-y-0 -start-12 my-auto"
|
||||||
: "-top-12 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 rotate-90",
|
: "start-1/2 -top-12 -translate-x-1/2 rotate-90 rtl:translate-x-1/2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollPrev}
|
disabled={!canScrollPrev}
|
||||||
@@ -194,7 +195,9 @@ function CarouselPrevious({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="rtl:rotate-180" />
|
<ChevronLeftIcon className="rtl:rotate-180" />
|
||||||
<span className="sr-only">Previous slide</span>
|
<span className="sr-only">
|
||||||
|
<LocalizedText message={uiMessages.previousSlide} />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -216,7 +219,7 @@ function CarouselNext({
|
|||||||
"absolute touch-manipulation rounded-full",
|
"absolute touch-manipulation rounded-full",
|
||||||
orientation === "horizontal"
|
orientation === "horizontal"
|
||||||
? "inset-y-0 -end-12 my-auto"
|
? "inset-y-0 -end-12 my-auto"
|
||||||
: "-bottom-12 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 rotate-90",
|
: "start-1/2 -bottom-12 -translate-x-1/2 rotate-90 rtl:translate-x-1/2",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={!canScrollNext}
|
disabled={!canScrollNext}
|
||||||
@@ -224,7 +227,9 @@ function CarouselNext({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronRightIcon className="rtl:rotate-180" />
|
<ChevronRightIcon className="rtl:rotate-180" />
|
||||||
<span className="sr-only">Next slide</span>
|
<span className="sr-only">
|
||||||
|
<LocalizedText message={uiMessages.nextSlide} />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
|||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from "lucide-react"
|
||||||
|
import { LocalizedText, uiMessages } from "./i18n"
|
||||||
|
|
||||||
type DialogHandle<Payload> = DialogPrimitive.Handle<Payload>
|
type DialogHandle<Payload> = DialogPrimitive.Handle<Payload>
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Popup
|
<DialogPrimitive.Popup
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-1/2 start-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 rtl:translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
"fixed start-1/2 top-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md rtl:translate-x-1/2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -71,14 +72,15 @@ function DialogContent({
|
|||||||
render={
|
render={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="absolute top-4 end-4"
|
className="absolute end-4 top-4"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<XIcon
|
<XIcon />
|
||||||
/>
|
<span className="sr-only">
|
||||||
<span className="sr-only">Close</span>
|
<LocalizedText message={uiMessages.close} />
|
||||||
|
</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Popup>
|
</DialogPrimitive.Popup>
|
||||||
@@ -116,7 +118,7 @@ function DialogFooter({
|
|||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||||
Close
|
<LocalizedText message={uiMessages.close} />
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import type { Locale as DayPickerLocale } from "react-day-picker"
|
||||||
|
|
||||||
|
export type MessageValues = Record<string, unknown>
|
||||||
|
|
||||||
|
export interface MessageDescriptor {
|
||||||
|
id: string
|
||||||
|
message: string
|
||||||
|
comment?: string
|
||||||
|
values?: MessageValues
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TranslateFunction = (
|
||||||
|
descriptor: MessageDescriptor,
|
||||||
|
values?: MessageValues
|
||||||
|
) => string
|
||||||
|
|
||||||
|
export type CalendarLocale = Partial<DayPickerLocale>
|
||||||
|
|
||||||
|
type UiMessages = Record<string, MessageDescriptor>
|
||||||
|
|
||||||
|
export const uiMessages = {
|
||||||
|
breadcrumb: /* i18n */ {
|
||||||
|
id: "ui.breadcrumb.label",
|
||||||
|
message: "Breadcrumb",
|
||||||
|
},
|
||||||
|
close: /* i18n */ {
|
||||||
|
id: "ui.common.close",
|
||||||
|
message: "Close",
|
||||||
|
},
|
||||||
|
closeToast: /* i18n */ {
|
||||||
|
id: "ui.toast.close",
|
||||||
|
message: "Close notification",
|
||||||
|
},
|
||||||
|
loading: /* i18n */ {
|
||||||
|
id: "ui.common.loading",
|
||||||
|
message: "Loading",
|
||||||
|
},
|
||||||
|
more: /* i18n */ {
|
||||||
|
id: "ui.common.more",
|
||||||
|
message: "More",
|
||||||
|
},
|
||||||
|
morePages: /* i18n */ {
|
||||||
|
id: "ui.pagination.more",
|
||||||
|
message: "More pages",
|
||||||
|
},
|
||||||
|
next: /* i18n */ {
|
||||||
|
id: "ui.pagination.next",
|
||||||
|
message: "Next",
|
||||||
|
},
|
||||||
|
nextPage: /* i18n */ {
|
||||||
|
id: "ui.pagination.nextPage",
|
||||||
|
message: "Go to next page",
|
||||||
|
},
|
||||||
|
nextSlide: /* i18n */ {
|
||||||
|
id: "ui.carousel.nextSlide",
|
||||||
|
message: "Next slide",
|
||||||
|
},
|
||||||
|
pagination: /* i18n */ {
|
||||||
|
id: "ui.pagination.label",
|
||||||
|
message: "Pagination",
|
||||||
|
},
|
||||||
|
previous: /* i18n */ {
|
||||||
|
id: "ui.pagination.previous",
|
||||||
|
message: "Previous",
|
||||||
|
},
|
||||||
|
previousPage: /* i18n */ {
|
||||||
|
id: "ui.pagination.previousPage",
|
||||||
|
message: "Go to previous page",
|
||||||
|
},
|
||||||
|
previousSlide: /* i18n */ {
|
||||||
|
id: "ui.carousel.previousSlide",
|
||||||
|
message: "Previous slide",
|
||||||
|
},
|
||||||
|
sidebar: /* i18n */ {
|
||||||
|
id: "ui.sidebar.title",
|
||||||
|
message: "Sidebar",
|
||||||
|
},
|
||||||
|
sidebarDescription: /* i18n */ {
|
||||||
|
id: "ui.sidebar.description",
|
||||||
|
message: "Displays the mobile sidebar.",
|
||||||
|
},
|
||||||
|
toggleSidebar: /* i18n */ {
|
||||||
|
id: "ui.sidebar.toggle",
|
||||||
|
message: "Toggle sidebar",
|
||||||
|
},
|
||||||
|
} satisfies UiMessages
|
||||||
|
|
||||||
|
const DEFAULT_LOCALE = "en-US"
|
||||||
|
const defaultTranslate: TranslateFunction = (descriptor) => descriptor.message
|
||||||
|
|
||||||
|
const TranslateContext =
|
||||||
|
React.createContext<TranslateFunction>(defaultTranslate)
|
||||||
|
const LocaleContext = React.createContext(DEFAULT_LOCALE)
|
||||||
|
const CalendarLocaleContext = React.createContext<CalendarLocale | undefined>(
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface I18nProviderProps {
|
||||||
|
calendarLocale?: CalendarLocale
|
||||||
|
children: React.ReactNode
|
||||||
|
locale?: string
|
||||||
|
t?: TranslateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
export function I18nProvider({
|
||||||
|
calendarLocale,
|
||||||
|
children,
|
||||||
|
locale = DEFAULT_LOCALE,
|
||||||
|
t = defaultTranslate,
|
||||||
|
}: I18nProviderProps) {
|
||||||
|
return (
|
||||||
|
<TranslateContext.Provider value={t}>
|
||||||
|
<LocaleContext.Provider value={locale}>
|
||||||
|
<CalendarLocaleContext.Provider value={calendarLocale}>
|
||||||
|
{children}
|
||||||
|
</CalendarLocaleContext.Provider>
|
||||||
|
</LocaleContext.Provider>
|
||||||
|
</TranslateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslate(): TranslateFunction {
|
||||||
|
return React.useContext(TranslateContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMessage(
|
||||||
|
descriptor: MessageDescriptor,
|
||||||
|
values?: MessageValues
|
||||||
|
) {
|
||||||
|
const translate = useTranslate()
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
() => translate(descriptor, values),
|
||||||
|
[descriptor, translate, values]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalizedTextProps {
|
||||||
|
message: MessageDescriptor
|
||||||
|
values?: MessageValues
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LocalizedText({ message, values }: LocalizedTextProps) {
|
||||||
|
return useMessage(message, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLocale(): string {
|
||||||
|
return React.useContext(LocaleContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCalendarLocale(): CalendarLocale | undefined {
|
||||||
|
return React.useContext(CalendarLocaleContext)
|
||||||
|
}
|
||||||
@@ -2,13 +2,24 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { LocalizedText, uiMessages, useMessage } from "./i18n"
|
||||||
|
|
||||||
|
function Pagination({
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"nav">) {
|
||||||
|
const defaultLabel = useMessage(uiMessages.pagination)
|
||||||
|
|
||||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label="pagination"
|
aria-label={ariaLabel ?? defaultLabel}
|
||||||
data-slot="pagination"
|
data-slot="pagination"
|
||||||
className={cn("mx-auto flex w-full justify-center", className)}
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -64,35 +75,43 @@ function PaginationLink({
|
|||||||
|
|
||||||
function PaginationPrevious({
|
function PaginationPrevious({
|
||||||
className,
|
className,
|
||||||
text = "Previous",
|
text,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||||
|
const previousLabel = useMessage(uiMessages.previous)
|
||||||
|
const previousPageLabel = useMessage(uiMessages.previousPage)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to previous page"
|
aria-label={ariaLabel ?? previousPageLabel}
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("ps-2!", className)}
|
className={cn("ps-2!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon data-icon="inline-start" className="rtl:rotate-180" />
|
<ChevronLeftIcon data-icon="inline-start" className="rtl:rotate-180" />
|
||||||
<span className="hidden sm:block">{text}</span>
|
<span className="hidden sm:block">{text ?? previousLabel}</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationNext({
|
function PaginationNext({
|
||||||
className,
|
className,
|
||||||
text = "Next",
|
text,
|
||||||
|
"aria-label": ariaLabel,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
||||||
|
const nextLabel = useMessage(uiMessages.next)
|
||||||
|
const nextPageLabel = useMessage(uiMessages.nextPage)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to next page"
|
aria-label={ariaLabel ?? nextPageLabel}
|
||||||
size="default"
|
size="default"
|
||||||
className={cn("pe-2!", className)}
|
className={cn("pe-2!", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:block">{text}</span>
|
<span className="hidden sm:block">{text ?? nextLabel}</span>
|
||||||
<ChevronRightIcon data-icon="inline-end" className="rtl:rotate-180" />
|
<ChevronRightIcon data-icon="inline-end" className="rtl:rotate-180" />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
@@ -112,9 +131,10 @@ function PaginationEllipsis({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon
|
<MoreHorizontalIcon />
|
||||||
/>
|
<span className="sr-only">
|
||||||
<span className="sr-only">More pages</span>
|
<LocalizedText message={uiMessages.morePages} />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
|
|||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from "lucide-react"
|
||||||
|
import { LocalizedText, uiMessages } from "./i18n"
|
||||||
|
|
||||||
function createSheetHandle<Payload>(): SheetPrimitive.Handle<Payload> {
|
function createSheetHandle<Payload>(): SheetPrimitive.Handle<Payload> {
|
||||||
return SheetPrimitive.createHandle<Payload>()
|
return SheetPrimitive.createHandle<Payload>()
|
||||||
@@ -75,7 +76,9 @@ function SheetContent({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">
|
||||||
|
<LocalizedText message={uiMessages.close} />
|
||||||
|
</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</SheetPrimitive.Popup>
|
</SheetPrimitive.Popup>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@workspace/ui/components/tooltip"
|
} from "@workspace/ui/components/tooltip"
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
import { PanelLeftIcon } from "lucide-react"
|
||||||
|
import { LocalizedText, uiMessages, useMessage } from "./i18n"
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||||
@@ -194,8 +195,12 @@ function Sidebar({
|
|||||||
side={side}
|
side={side}
|
||||||
>
|
>
|
||||||
<SheetHeader className="sr-only">
|
<SheetHeader className="sr-only">
|
||||||
<SheetTitle>Sidebar</SheetTitle>
|
<SheetTitle>
|
||||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
<LocalizedText message={uiMessages.sidebar} />
|
||||||
|
</SheetTitle>
|
||||||
|
<SheetDescription>
|
||||||
|
<LocalizedText message={uiMessages.sidebarDescription} />
|
||||||
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
@@ -270,27 +275,35 @@ function SidebarTrigger({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PanelLeftIcon className="rtl:rotate-180" />
|
<PanelLeftIcon className="rtl:rotate-180" />
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
<span className="sr-only">
|
||||||
|
<LocalizedText message={uiMessages.toggleSidebar} />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
function SidebarRail({
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button">) {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar()
|
||||||
|
const toggleSidebarLabel = useMessage(uiMessages.toggleSidebar)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
data-sidebar="rail"
|
data-sidebar="rail"
|
||||||
data-slot="sidebar-rail"
|
data-slot="sidebar-rail"
|
||||||
aria-label="Toggle Sidebar"
|
aria-label={ariaLabel ?? toggleSidebarLabel}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
title="Toggle Sidebar"
|
title={title ?? toggleSidebarLabel}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||||
"in-data-[side=left]:cursor-w-resize rtl:in-data-[side=left]:cursor-e-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize",
|
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=left]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize",
|
||||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize rtl:[[data-side=left][data-state=collapsed]_&]:cursor-w-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize rtl:[[data-side=right][data-state=collapsed]_&]:cursor-e-resize",
|
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize rtl:[[data-side=left][data-state=collapsed]_&]:cursor-w-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize rtl:[[data-side=right][data-state=collapsed]_&]:cursor-e-resize",
|
||||||
"group-data-[collapsible=offcanvas]:translate-x-0 rtl:group-data-[collapsible=offcanvas]:-translate-x-0 group-data-[collapsible=offcanvas]:after:start-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
|
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:start-full hover:group-data-[collapsible=offcanvas]:bg-sidebar rtl:group-data-[collapsible=offcanvas]:-translate-x-0",
|
||||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-end-2",
|
"[[data-side=left][data-collapsible=offcanvas]_&]:-end-2",
|
||||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-start-2",
|
"[[data-side=right][data-collapsible=offcanvas]_&]:-start-2",
|
||||||
className
|
className
|
||||||
@@ -422,7 +435,7 @@ function SidebarGroupAction({
|
|||||||
props: mergeProps<"button">(
|
props: mergeProps<"button">(
|
||||||
{
|
{
|
||||||
className: cn(
|
className: cn(
|
||||||
"absolute top-3.5 end-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
"absolute end-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
className
|
className
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -562,7 +575,7 @@ function SidebarMenuAction({
|
|||||||
props: mergeProps<"button">(
|
props: mergeProps<"button">(
|
||||||
{
|
{
|
||||||
className: cn(
|
className: cn(
|
||||||
"absolute top-1.5 end-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
"absolute end-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
showOnHover &&
|
showOnHover &&
|
||||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
|
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
|
||||||
className
|
className
|
||||||
@@ -639,7 +652,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
|||||||
data-slot="sidebar-menu-sub"
|
data-slot="sidebar-menu-sub"
|
||||||
data-sidebar="menu-sub"
|
data-sidebar="menu-sub"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-3.5 flex min-w-0 translate-x-px rtl:-translate-x-px flex-col gap-1 border-s border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
|
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-s border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden rtl:-translate-x-px",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -677,7 +690,7 @@ function SidebarMenuSubButton({
|
|||||||
props: mergeProps<"a">(
|
props: mergeProps<"a">(
|
||||||
{
|
{
|
||||||
className: cn(
|
className: cn(
|
||||||
"flex h-7 min-w-0 -translate-x-px rtl:translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs rtl:translate-x-px data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||||
className
|
className
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { Loader2Icon } from "lucide-react"
|
import { Loader2Icon } from "lucide-react"
|
||||||
|
import { uiMessages, useMessage } from "./i18n"
|
||||||
|
|
||||||
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||||
|
const loadingLabel = useMessage(uiMessages.loading)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Loader2Icon data-slot="spinner" role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />
|
<Loader2Icon
|
||||||
|
data-slot="spinner"
|
||||||
|
role="status"
|
||||||
|
aria-label={loadingLabel}
|
||||||
|
className={cn("size-4 animate-spin", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ import { Toast as ToastPrimitive } from "@base-ui/react/toast"
|
|||||||
|
|
||||||
import { cn } from "@workspace/ui/lib/utils"
|
import { cn } from "@workspace/ui/lib/utils"
|
||||||
import { Button } from "@workspace/ui/components/button"
|
import { Button } from "@workspace/ui/components/button"
|
||||||
import { XIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
import {
|
||||||
|
XIcon,
|
||||||
|
CircleCheckIcon,
|
||||||
|
InfoIcon,
|
||||||
|
TriangleAlertIcon,
|
||||||
|
OctagonXIcon,
|
||||||
|
Loader2Icon,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { uiMessages, useMessage } from "./i18n"
|
||||||
|
|
||||||
const toast = ToastPrimitive.createToastManager()
|
const toast = ToastPrimitive.createToastManager()
|
||||||
|
|
||||||
@@ -20,7 +28,7 @@ function ToastViewport({ className, ...props }: ToastPrimitive.Viewport.Props) {
|
|||||||
<ToastPrimitive.Viewport
|
<ToastPrimitive.Viewport
|
||||||
data-slot="toast-viewport"
|
data-slot="toast-viewport"
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:end-4 sm:start-auto sm:mx-0 sm:w-full",
|
"pointer-events-none fixed inset-x-4 bottom-4 z-50 mx-auto w-auto max-w-sm outline-none sm:start-auto sm:end-4 sm:mx-0 sm:w-full",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -35,19 +43,19 @@ function Toast({ className, ...props }: ToastPrimitive.Root.Props) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"group/toast pointer-events-auto absolute end-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
"group/toast pointer-events-auto absolute end-0 bottom-0 z-[calc(1000-var(--toast-index))] w-full origin-bottom rounded-2xl border bg-popover text-popover-foreground shadow-lg will-change-transform outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||||
"[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]",
|
"[--gap:0.75rem] [--height:var(--toast-frontmost-height,var(--toast-height))] [--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] [--peek:0.75rem] [--scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--shrink:calc(1-var(--scale))]",
|
||||||
"h-(--height) [transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]",
|
"h-(--height) transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] [transition:transform_500ms_cubic-bezier(0.22,1,0.36,1),opacity_500ms,height_150ms]",
|
||||||
"after:absolute after:top-full after:start-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
|
"after:absolute after:start-0 after:top-full after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']",
|
||||||
"data-expanded:h-(--toast-height) data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]",
|
"data-expanded:h-(--toast-height) data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--offset-y))]",
|
||||||
"data-limited:opacity-0 data-starting-style:[transform:translateY(150%)]",
|
"data-limited:opacity-0 data-starting-style:transform-[translateY(150%)]",
|
||||||
"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)]",
|
"[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:transform-[translateY(150%)]",
|
||||||
"data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
"data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
||||||
"data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
"data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
||||||
"data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
"data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
||||||
"data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
"data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
"data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+150%))]",
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
"data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))]",
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
"data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))]",
|
||||||
"data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
"data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-150%))]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -107,15 +115,18 @@ function ToastAction({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ToastClose({
|
function ToastClose({
|
||||||
|
"aria-label": ariaLabel,
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
render = <Button variant="ghost" size="icon-sm" />,
|
render = <Button variant="ghost" size="icon-sm" />,
|
||||||
...props
|
...props
|
||||||
}: ToastPrimitive.Close.Props) {
|
}: ToastPrimitive.Close.Props) {
|
||||||
|
const closeLabel = useMessage(uiMessages.closeToast)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastPrimitive.Close
|
<ToastPrimitive.Close
|
||||||
data-slot="toast-close"
|
data-slot="toast-close"
|
||||||
aria-label="Close toast"
|
aria-label={ariaLabel ?? closeLabel}
|
||||||
render={render}
|
render={render}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground",
|
"relative shrink-0 text-muted-foreground after:absolute after:-inset-2 after:content-[''] hover:text-foreground",
|
||||||
@@ -123,9 +134,7 @@ function ToastClose({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children ?? (
|
{children ?? <XIcon aria-hidden="true" />}
|
||||||
<XIcon aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</ToastPrimitive.Close>
|
</ToastPrimitive.Close>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -134,33 +143,23 @@ function ToastIcon({ type }: { type: string | undefined }) {
|
|||||||
let icon: React.ReactNode = null
|
let icon: React.ReactNode = null
|
||||||
|
|
||||||
if (type === "success") {
|
if (type === "success") {
|
||||||
icon = (
|
icon = <CircleCheckIcon aria-hidden="true" />
|
||||||
<CircleCheckIcon aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "info") {
|
if (type === "info") {
|
||||||
icon = (
|
icon = <InfoIcon aria-hidden="true" />
|
||||||
<InfoIcon aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "warning") {
|
if (type === "warning") {
|
||||||
icon = (
|
icon = <TriangleAlertIcon aria-hidden="true" />
|
||||||
<TriangleAlertIcon aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "error") {
|
if (type === "error") {
|
||||||
icon = (
|
icon = <OctagonXIcon className="text-destructive" aria-hidden="true" />
|
||||||
<OctagonXIcon className="text-destructive" aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "loading") {
|
if (type === "loading") {
|
||||||
icon = (
|
icon = <Loader2Icon className="animate-spin" aria-hidden="true" />
|
||||||
<Loader2Icon className="animate-spin" aria-hidden="true" />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export const uiCatalogLocales = [
|
||||||
|
"de",
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"ja",
|
||||||
|
"ko",
|
||||||
|
"zh-Hans",
|
||||||
|
"zh-Hant",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const uiMessageIds = [
|
||||||
|
"ui.breadcrumb.label",
|
||||||
|
"ui.carousel.nextSlide",
|
||||||
|
"ui.carousel.previousSlide",
|
||||||
|
"ui.common.close",
|
||||||
|
"ui.common.loading",
|
||||||
|
"ui.common.more",
|
||||||
|
"ui.pagination.label",
|
||||||
|
"ui.pagination.more",
|
||||||
|
"ui.pagination.next",
|
||||||
|
"ui.pagination.nextPage",
|
||||||
|
"ui.pagination.previous",
|
||||||
|
"ui.pagination.previousPage",
|
||||||
|
"ui.sidebar.description",
|
||||||
|
"ui.sidebar.title",
|
||||||
|
"ui.sidebar.toggle",
|
||||||
|
"ui.toast.close",
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type UiCatalogLocale = (typeof uiCatalogLocales)[number]
|
||||||
|
export type UiMessageId = (typeof uiMessageIds)[number]
|
||||||
|
export type UiMessageCatalog = Readonly<Record<UiMessageId, string>>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { de as calendarLocale } from "react-day-picker/locale/de"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "de"
|
||||||
|
export const languageTag = "de-DE"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "Breadcrumb-Navigation",
|
||||||
|
"ui.carousel.nextSlide": "Nächste Folie",
|
||||||
|
"ui.carousel.previousSlide": "Vorherige Folie",
|
||||||
|
"ui.common.close": "Schließen",
|
||||||
|
"ui.common.loading": "Wird geladen",
|
||||||
|
"ui.common.more": "Mehr",
|
||||||
|
"ui.pagination.label": "Seitennavigation",
|
||||||
|
"ui.pagination.more": "Weitere Seiten",
|
||||||
|
"ui.pagination.next": "Weiter",
|
||||||
|
"ui.pagination.nextPage": "Zur nächsten Seite",
|
||||||
|
"ui.pagination.previous": "Zurück",
|
||||||
|
"ui.pagination.previousPage": "Zur vorherigen Seite",
|
||||||
|
"ui.sidebar.description": "Zeigt die mobile Seitenleiste an.",
|
||||||
|
"ui.sidebar.title": "Seitenleiste",
|
||||||
|
"ui.sidebar.toggle": "Seitenleiste umschalten",
|
||||||
|
"ui.toast.close": "Benachrichtigung schließen",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { enUS as calendarLocale } from "react-day-picker/locale/en-US"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "en"
|
||||||
|
export const languageTag = "en-US"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "Breadcrumb",
|
||||||
|
"ui.carousel.nextSlide": "Next slide",
|
||||||
|
"ui.carousel.previousSlide": "Previous slide",
|
||||||
|
"ui.common.close": "Close",
|
||||||
|
"ui.common.loading": "Loading",
|
||||||
|
"ui.common.more": "More",
|
||||||
|
"ui.pagination.label": "Pagination",
|
||||||
|
"ui.pagination.more": "More pages",
|
||||||
|
"ui.pagination.next": "Next",
|
||||||
|
"ui.pagination.nextPage": "Go to next page",
|
||||||
|
"ui.pagination.previous": "Previous",
|
||||||
|
"ui.pagination.previousPage": "Go to previous page",
|
||||||
|
"ui.sidebar.description": "Displays the mobile sidebar.",
|
||||||
|
"ui.sidebar.title": "Sidebar",
|
||||||
|
"ui.sidebar.toggle": "Toggle sidebar",
|
||||||
|
"ui.toast.close": "Close notification",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { es as calendarLocale } from "react-day-picker/locale/es"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "es"
|
||||||
|
export const languageTag = "es-ES"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "Ruta de navegación",
|
||||||
|
"ui.carousel.nextSlide": "Diapositiva siguiente",
|
||||||
|
"ui.carousel.previousSlide": "Diapositiva anterior",
|
||||||
|
"ui.common.close": "Cerrar",
|
||||||
|
"ui.common.loading": "Cargando",
|
||||||
|
"ui.common.more": "Más",
|
||||||
|
"ui.pagination.label": "Paginación",
|
||||||
|
"ui.pagination.more": "Más páginas",
|
||||||
|
"ui.pagination.next": "Siguiente",
|
||||||
|
"ui.pagination.nextPage": "Ir a la página siguiente",
|
||||||
|
"ui.pagination.previous": "Anterior",
|
||||||
|
"ui.pagination.previousPage": "Ir a la página anterior",
|
||||||
|
"ui.sidebar.description": "Muestra la barra lateral móvil.",
|
||||||
|
"ui.sidebar.title": "Barra lateral",
|
||||||
|
"ui.sidebar.toggle": "Alternar barra lateral",
|
||||||
|
"ui.toast.close": "Cerrar notificación",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { fr as calendarLocale } from "react-day-picker/locale/fr"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "fr"
|
||||||
|
export const languageTag = "fr-FR"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "Fil d’Ariane",
|
||||||
|
"ui.carousel.nextSlide": "Diapositive suivante",
|
||||||
|
"ui.carousel.previousSlide": "Diapositive précédente",
|
||||||
|
"ui.common.close": "Fermer",
|
||||||
|
"ui.common.loading": "Chargement",
|
||||||
|
"ui.common.more": "Plus",
|
||||||
|
"ui.pagination.label": "Pagination",
|
||||||
|
"ui.pagination.more": "Plus de pages",
|
||||||
|
"ui.pagination.next": "Suivant",
|
||||||
|
"ui.pagination.nextPage": "Aller à la page suivante",
|
||||||
|
"ui.pagination.previous": "Précédent",
|
||||||
|
"ui.pagination.previousPage": "Aller à la page précédente",
|
||||||
|
"ui.sidebar.description": "Affiche la barre latérale mobile.",
|
||||||
|
"ui.sidebar.title": "Barre latérale",
|
||||||
|
"ui.sidebar.toggle": "Afficher ou masquer la barre latérale",
|
||||||
|
"ui.toast.close": "Fermer la notification",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { ja as calendarLocale } from "react-day-picker/locale/ja"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "ja"
|
||||||
|
export const languageTag = "ja-JP"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "パンくずリスト",
|
||||||
|
"ui.carousel.nextSlide": "次のスライド",
|
||||||
|
"ui.carousel.previousSlide": "前のスライド",
|
||||||
|
"ui.common.close": "閉じる",
|
||||||
|
"ui.common.loading": "読み込み中",
|
||||||
|
"ui.common.more": "その他",
|
||||||
|
"ui.pagination.label": "ページネーション",
|
||||||
|
"ui.pagination.more": "その他のページ",
|
||||||
|
"ui.pagination.next": "次へ",
|
||||||
|
"ui.pagination.nextPage": "次のページへ移動",
|
||||||
|
"ui.pagination.previous": "前へ",
|
||||||
|
"ui.pagination.previousPage": "前のページへ移動",
|
||||||
|
"ui.sidebar.description": "モバイルサイドバーを表示します。",
|
||||||
|
"ui.sidebar.title": "サイドバー",
|
||||||
|
"ui.sidebar.toggle": "サイドバーを切り替える",
|
||||||
|
"ui.toast.close": "通知を閉じる",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { ko as calendarLocale } from "react-day-picker/locale/ko"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "ko"
|
||||||
|
export const languageTag = "ko-KR"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "이동 경로",
|
||||||
|
"ui.carousel.nextSlide": "다음 슬라이드",
|
||||||
|
"ui.carousel.previousSlide": "이전 슬라이드",
|
||||||
|
"ui.common.close": "닫기",
|
||||||
|
"ui.common.loading": "불러오는 중",
|
||||||
|
"ui.common.more": "더 보기",
|
||||||
|
"ui.pagination.label": "페이지 탐색",
|
||||||
|
"ui.pagination.more": "더 많은 페이지",
|
||||||
|
"ui.pagination.next": "다음",
|
||||||
|
"ui.pagination.nextPage": "다음 페이지로 이동",
|
||||||
|
"ui.pagination.previous": "이전",
|
||||||
|
"ui.pagination.previousPage": "이전 페이지로 이동",
|
||||||
|
"ui.sidebar.description": "모바일 사이드바를 표시합니다.",
|
||||||
|
"ui.sidebar.title": "사이드바",
|
||||||
|
"ui.sidebar.toggle": "사이드바 전환",
|
||||||
|
"ui.toast.close": "알림 닫기",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { zhCN as calendarLocale } from "react-day-picker/locale/zh-CN"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hans"
|
||||||
|
export const languageTag = "zh-CN"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "面包屑导航",
|
||||||
|
"ui.carousel.nextSlide": "下一张",
|
||||||
|
"ui.carousel.previousSlide": "上一张",
|
||||||
|
"ui.common.close": "关闭",
|
||||||
|
"ui.common.loading": "正在加载",
|
||||||
|
"ui.common.more": "更多",
|
||||||
|
"ui.pagination.label": "分页导航",
|
||||||
|
"ui.pagination.more": "更多页面",
|
||||||
|
"ui.pagination.next": "下一页",
|
||||||
|
"ui.pagination.nextPage": "前往下一页",
|
||||||
|
"ui.pagination.previous": "上一页",
|
||||||
|
"ui.pagination.previousPage": "前往上一页",
|
||||||
|
"ui.sidebar.description": "显示移动端侧栏。",
|
||||||
|
"ui.sidebar.title": "侧栏",
|
||||||
|
"ui.sidebar.toggle": "切换侧栏",
|
||||||
|
"ui.toast.close": "关闭通知",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export { zhTW as calendarLocale } from "react-day-picker/locale/zh-TW"
|
||||||
|
|
||||||
|
import type { UiMessageCatalog } from "../catalogs"
|
||||||
|
|
||||||
|
export const locale = "zh-Hant"
|
||||||
|
export const languageTag = "zh-TW"
|
||||||
|
|
||||||
|
export const messages = {
|
||||||
|
"ui.breadcrumb.label": "麵包屑導覽",
|
||||||
|
"ui.carousel.nextSlide": "下一張",
|
||||||
|
"ui.carousel.previousSlide": "上一張",
|
||||||
|
"ui.common.close": "關閉",
|
||||||
|
"ui.common.loading": "載入中",
|
||||||
|
"ui.common.more": "更多",
|
||||||
|
"ui.pagination.label": "分頁導覽",
|
||||||
|
"ui.pagination.more": "更多頁面",
|
||||||
|
"ui.pagination.next": "下一頁",
|
||||||
|
"ui.pagination.nextPage": "前往下一頁",
|
||||||
|
"ui.pagination.previous": "上一頁",
|
||||||
|
"ui.pagination.previousPage": "前往上一頁",
|
||||||
|
"ui.sidebar.description": "顯示行動版側欄。",
|
||||||
|
"ui.sidebar.title": "側欄",
|
||||||
|
"ui.sidebar.toggle": "切換側欄",
|
||||||
|
"ui.toast.close": "關閉通知",
|
||||||
|
} as const satisfies UiMessageCatalog
|
||||||
Reference in New Issue
Block a user