feat: publish HoloLake model-native living system source
This commit is contained in:
parent
6ad10edde1
commit
c395dd3a99
2467 changed files with 615073 additions and 0 deletions
|
|
@ -0,0 +1,78 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { useCallback, useLayoutEffect, useState, type ReactNode } from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { isRecoveredActionTooltipError } from './actionTooltipRecovery'
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock('./tooltip')
|
||||
vi.resetModules()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('ActionTooltip recovery', () => {
|
||||
it('keeps the trigger mounted when tooltip content rendering fails', async () => {
|
||||
const tooltipError = new Error('tooltip content render failed')
|
||||
vi.doMock('./tooltip', () => {
|
||||
return {
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipContent: () => {
|
||||
throw tooltipError
|
||||
},
|
||||
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
}
|
||||
})
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const { ActionTooltip } = await import('./action-tooltip')
|
||||
|
||||
render(
|
||||
<ActionTooltip copy={{ label: 'Switch editor layout' }}>
|
||||
<button type="button">Switch editor layout</button>
|
||||
</ActionTooltip>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Switch editor layout' })).toBeInTheDocument()
|
||||
expect(isRecoveredActionTooltipError(tooltipError)).toBe(true)
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps failed tooltip content disabled when fallback mounting updates parent state', async () => {
|
||||
const tooltipError = new Error('tooltip content render failed')
|
||||
vi.doMock('./tooltip', () => {
|
||||
return {
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipContent: () => {
|
||||
throw tooltipError
|
||||
},
|
||||
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
}
|
||||
})
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const { ActionTooltip } = await import('./action-tooltip')
|
||||
|
||||
function FallbackButton({ onMount }: { onMount: () => void }) {
|
||||
useLayoutEffect(() => {
|
||||
onMount()
|
||||
}, [onMount])
|
||||
return <button type="button">Switch editor layout</button>
|
||||
}
|
||||
|
||||
function ParentWithFallbackStateUpdate() {
|
||||
const [renderCount, setRenderCount] = useState(0)
|
||||
const bumpRenderCount = useCallback(() => {
|
||||
setRenderCount((current) => current + 1)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ActionTooltip copy={{ label: `Switch editor layout ${renderCount}` }}>
|
||||
<FallbackButton onMount={bumpRenderCount} />
|
||||
</ActionTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
expect(() => render(<ParentWithFallbackStateUpdate />)).not.toThrow(/Maximum update depth/)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Switch editor layout' })).toBeInTheDocument()
|
||||
expect(isRecoveredActionTooltipError(tooltipError)).toBe(true)
|
||||
expect(consoleError).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { Component, type ComponentProps, type ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { markRecoveredActionTooltipError } from './actionTooltipRecovery'
|
||||
|
||||
export interface ActionTooltipCopy {
|
||||
label: string
|
||||
shortcut?: string
|
||||
}
|
||||
|
||||
export interface ActionTooltipProps {
|
||||
copy: ActionTooltipCopy
|
||||
children: ReactNode
|
||||
className?: string
|
||||
contentTestId?: string
|
||||
side?: ComponentProps<typeof TooltipContent>['side']
|
||||
align?: ComponentProps<typeof TooltipContent>['align']
|
||||
sideOffset?: number
|
||||
open?: ComponentProps<typeof Tooltip>['open']
|
||||
onOpenChange?: ComponentProps<typeof Tooltip>['onOpenChange']
|
||||
}
|
||||
|
||||
interface ActionTooltipBoundaryProps {
|
||||
children: ReactNode
|
||||
fallback: ReactNode
|
||||
}
|
||||
|
||||
interface ActionTooltipBoundaryState {
|
||||
failed: boolean
|
||||
}
|
||||
|
||||
class ActionTooltipBoundary extends Component<ActionTooltipBoundaryProps, ActionTooltipBoundaryState> {
|
||||
state: ActionTooltipBoundaryState = { failed: false }
|
||||
|
||||
static getDerivedStateFromError(): ActionTooltipBoundaryState {
|
||||
return { failed: true }
|
||||
}
|
||||
|
||||
componentDidCatch(error: unknown) {
|
||||
markRecoveredActionTooltipError(error)
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.failed ? this.props.fallback : this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
export function ActionTooltip({
|
||||
copy,
|
||||
children,
|
||||
className,
|
||||
contentTestId,
|
||||
side = 'top',
|
||||
align = 'center',
|
||||
sideOffset = 6,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: ActionTooltipProps) {
|
||||
return (
|
||||
<ActionTooltipBoundary fallback={children}>
|
||||
<Tooltip open={open} onOpenChange={onOpenChange}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
data-align={align}
|
||||
data-testid={contentTestId}
|
||||
className={cn('px-2.5 py-2', className)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="min-w-0 flex-1 text-[11px] font-medium leading-tight">{copy.label}</span>
|
||||
{copy.shortcut && (
|
||||
<span className="shrink-0 rounded border border-background/20 bg-background/10 px-1.5 py-0.5 font-mono text-[10px] leading-none text-background/80">
|
||||
{copy.shortcut}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</ActionTooltipBoundary>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
const ACTION_TOOLTIP_RECOVERY_BOUNDARY_NAME = 'ActionTooltipBoundary'
|
||||
const RECOVERED_ACTION_TOOLTIP_ERROR_MARK = '__tolariaRecoveredActionTooltipError'
|
||||
|
||||
type MarkedRecoveredActionTooltipError = Error & {
|
||||
[RECOVERED_ACTION_TOOLTIP_ERROR_MARK]?: true
|
||||
}
|
||||
|
||||
function hasRecoveredActionTooltipMark(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) return false
|
||||
return Reflect.get(error as MarkedRecoveredActionTooltipError, RECOVERED_ACTION_TOOLTIP_ERROR_MARK) === true
|
||||
}
|
||||
|
||||
export function markRecoveredActionTooltipError(error: unknown): void {
|
||||
if (!(error instanceof Error)) return
|
||||
Reflect.set(error as MarkedRecoveredActionTooltipError, RECOVERED_ACTION_TOOLTIP_ERROR_MARK, true)
|
||||
}
|
||||
|
||||
export function isRecoveredActionTooltipError(error: unknown, componentStack = ''): boolean {
|
||||
return hasRecoveredActionTooltipMark(error)
|
||||
|| componentStack.includes(ACTION_TOOLTIP_RECOVERY_BOUNDARY_NAME)
|
||||
}
|
||||
49
product-source/hololake-platform/src/components/ui/badge.tsx
Normal file
49
product-source/hololake-platform/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- shadcn/ui pattern
|
||||
export { Badge, badgeVariants }
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-4",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- shadcn/ui pattern
|
||||
export { Button, buttonVariants }
|
||||
272
product-source/hololake-platform/src/components/ui/calendar.tsx
Normal file
272
product-source/hololake-platform/src/components/ui/calendar.tsx
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
CaretDown as ChevronDownIcon,
|
||||
CaretLeft as ChevronLeftIcon,
|
||||
CaretRight as ChevronRightIcon,
|
||||
} from '@phosphor-icons/react'
|
||||
import {
|
||||
DayPicker,
|
||||
getDefaultClassNames,
|
||||
type ChevronProps,
|
||||
type DayButton,
|
||||
type DropdownProps,
|
||||
type RootProps,
|
||||
type WeekNumberProps,
|
||||
} from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
|
||||
const CALENDAR_START_MONTH = new Date(1900, 0)
|
||||
const CALENDAR_END_MONTH = new Date(2100, 11)
|
||||
|
||||
type CalendarProps = React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "dropdown",
|
||||
navLayout,
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
startMonth = CALENDAR_START_MONTH,
|
||||
endMonth = CALENDAR_END_MONTH,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
navLayout={navLayout}
|
||||
startMonth={startMonth}
|
||||
endMonth={endMonth}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={getCalendarClassNames({
|
||||
defaultClassNames,
|
||||
classNames,
|
||||
captionLayout,
|
||||
buttonVariant,
|
||||
showWeekNumber: props.showWeekNumber,
|
||||
})}
|
||||
components={{
|
||||
Root: CalendarRoot,
|
||||
Chevron: CalendarChevron,
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: CalendarWeekNumber,
|
||||
Dropdown: CalendarDropdown,
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function getCalendarClassNames({
|
||||
defaultClassNames,
|
||||
classNames,
|
||||
captionLayout,
|
||||
buttonVariant,
|
||||
showWeekNumber,
|
||||
}: {
|
||||
defaultClassNames: ReturnType<typeof getDefaultClassNames>
|
||||
classNames: CalendarProps["classNames"]
|
||||
captionLayout: CalendarProps["captionLayout"]
|
||||
buttonVariant: CalendarProps["buttonVariant"]
|
||||
showWeekNumber: CalendarProps["showWeekNumber"]
|
||||
}) {
|
||||
return {
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn("flex gap-4 flex-col md:flex-row relative", defaultClassNames.months),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn("pointer-events-none flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between", defaultClassNames.nav),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"pointer-events-auto size-(--cell-size) aria-disabled:opacity-50 p-0 select-none hover:bg-transparent",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"pointer-events-auto size-(--cell-size) aria-disabled:opacity-50 p-0 select-none hover:bg-transparent",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn("absolute bg-popover inset-0 opacity-0", defaultClassNames.dropdown),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn("text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none", defaultClassNames.weekday),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn("select-none w-(--cell-size)", defaultClassNames.week_number_header),
|
||||
week_number: cn("text-[0.8rem] select-none text-muted-foreground", defaultClassNames.week_number),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn("bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none", defaultClassNames.today),
|
||||
outside: cn("text-muted-foreground aria-selected:text-muted-foreground", defaultClassNames.outside),
|
||||
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}
|
||||
}
|
||||
|
||||
function CalendarRoot({ className, rootRef, ...props }: RootProps) {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarChevron({ className, orientation, ...props }: ChevronProps) {
|
||||
if (orientation === "left") {
|
||||
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return <ChevronRightIcon className={cn("size-4", className)} {...props} />
|
||||
}
|
||||
|
||||
return <ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
}
|
||||
|
||||
function CalendarWeekNumber({ children, ...props }: WeekNumberProps) {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDropdown({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
"aria-label": ariaLabel,
|
||||
}: DropdownProps) {
|
||||
const handleValueChange = (newValue: string) => {
|
||||
const changeEvent = {
|
||||
target: { value: newValue },
|
||||
} as Parameters<NonNullable<DropdownProps['onChange']>>[0]
|
||||
onChange?.(changeEvent)
|
||||
}
|
||||
|
||||
return (
|
||||
<Select value={value?.toString()} onValueChange={handleValueChange}>
|
||||
<SelectTrigger
|
||||
aria-label={ariaLabel}
|
||||
size="sm"
|
||||
className="h-8 min-w-[88px] justify-center gap-1 bg-background px-2 py-0 text-sm font-medium"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[13000] max-h-64">
|
||||
<SelectGroup>
|
||||
{options?.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value.toString()}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
92
product-source/hololake-platform/src/components/ui/card.tsx
Normal file
92
product-source/hololake-platform/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react'
|
||||
import { Check } from '@phosphor-icons/react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type CheckedState = boolean | 'indeterminate'
|
||||
|
||||
interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'checked' | 'onChange' | 'type'> {
|
||||
checked?: CheckedState
|
||||
onCheckedChange?: (checked: CheckedState) => void
|
||||
}
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
checked = false,
|
||||
disabled = false,
|
||||
onCheckedChange,
|
||||
...props
|
||||
}: CheckboxProps) {
|
||||
const isChecked = checked === true
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (inputRef.current) inputRef.current.indeterminate = checked === 'indeterminate'
|
||||
}, [checked])
|
||||
|
||||
return (
|
||||
<span className="relative inline-flex size-4 shrink-0 items-center justify-center">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
aria-checked={checked === 'indeterminate' ? 'mixed' : isChecked}
|
||||
data-slot="checkbox"
|
||||
data-state={isChecked ? 'checked' : 'unchecked'}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 appearance-none rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
onChange={() => {
|
||||
if (disabled) return
|
||||
onCheckedChange?.(!isChecked)
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
{isChecked && (
|
||||
<span
|
||||
data-slot="checkbox-indicator"
|
||||
className="pointer-events-none absolute inset-0 flex items-center justify-center text-primary-foreground transition-none"
|
||||
>
|
||||
<Check className="size-3.5" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
171
product-source/hololake-platform/src/components/ui/dialog.tsx
Normal file
171
product-source/hololake-platform/src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import * as React from "react"
|
||||
import { X as XIcon } from '@phosphor-icons/react'
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
function dialogSlotProps(slot: string, baseClassName: string, className?: string) {
|
||||
return {
|
||||
"data-slot": slot,
|
||||
className: cn(baseClassName, className),
|
||||
}
|
||||
}
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type DialogTextPartProps =
|
||||
| (React.ComponentProps<typeof DialogPrimitive.Title> & { textPart: "title" })
|
||||
| (React.ComponentProps<typeof DialogPrimitive.Description> & { textPart: "description" })
|
||||
|
||||
function DialogTextPart(props: DialogTextPartProps) {
|
||||
if (props.textPart === "title") {
|
||||
const { textPart, className, ...titleProps } = props
|
||||
void textPart
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
{...dialogSlotProps("dialog-title", "text-lg leading-none font-semibold", className)}
|
||||
{...titleProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const { textPart, className, ...descriptionProps } = props
|
||||
void textPart
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
{...dialogSlotProps("dialog-description", "text-muted-foreground text-sm", className)}
|
||||
{...descriptionProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle(props: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return <DialogTextPart textPart="title" {...props} />
|
||||
}
|
||||
|
||||
function DialogDescription(props: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return <DialogTextPart textPart="description" {...props} />
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { CaretRight as ChevronRightIcon, Check as CheckIcon, Circle as CircleIcon } from '@phosphor-icons/react'
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const dropdownMenuContentMotionClass =
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-(--radix-dropdown-menu-content-transform-origin) rounded-md border p-1"
|
||||
|
||||
const dropdownMenuCheckedItemClass =
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
|
||||
const dropdownMenuItemIndicatorClass =
|
||||
"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"
|
||||
|
||||
function createDropdownMenuSlot<T extends React.ElementType>(
|
||||
Component: T,
|
||||
slot: string,
|
||||
displayName: string
|
||||
) {
|
||||
function DropdownMenuSlot(props: React.ComponentProps<T>) {
|
||||
return React.createElement(Component, { "data-slot": slot, ...props })
|
||||
}
|
||||
|
||||
DropdownMenuSlot.displayName = displayName
|
||||
return DropdownMenuSlot
|
||||
}
|
||||
|
||||
function DropdownMenuItemIndicator({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<span className={dropdownMenuItemIndicatorClass}>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function createDropdownMenuCheckedItem<T extends React.ElementType>(
|
||||
Component: T,
|
||||
slot: string,
|
||||
indicator: React.ReactNode,
|
||||
displayName: string
|
||||
) {
|
||||
type Props = React.ComponentProps<T> & {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
function CheckedItem({ className, children, ...props }: Props) {
|
||||
return React.createElement(
|
||||
Component,
|
||||
{
|
||||
"data-slot": slot,
|
||||
...props,
|
||||
className: cn(dropdownMenuCheckedItemClass, className),
|
||||
},
|
||||
<>
|
||||
<DropdownMenuItemIndicator>{indicator}</DropdownMenuItemIndicator>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
CheckedItem.displayName = displayName
|
||||
return CheckedItem
|
||||
}
|
||||
|
||||
const DropdownMenu = createDropdownMenuSlot(
|
||||
DropdownMenuPrimitive.Root,
|
||||
"dropdown-menu",
|
||||
"DropdownMenu"
|
||||
)
|
||||
|
||||
const DropdownMenuPortal = createDropdownMenuSlot(
|
||||
DropdownMenuPrimitive.Portal,
|
||||
"dropdown-menu-portal",
|
||||
"DropdownMenuPortal"
|
||||
)
|
||||
|
||||
const DropdownMenuTrigger = createDropdownMenuSlot(
|
||||
DropdownMenuPrimitive.Trigger,
|
||||
"dropdown-menu-trigger",
|
||||
"DropdownMenuTrigger"
|
||||
)
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
dropdownMenuContentMotionClass,
|
||||
"max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuGroup = createDropdownMenuSlot(
|
||||
DropdownMenuPrimitive.Group,
|
||||
"dropdown-menu-group",
|
||||
"DropdownMenuGroup"
|
||||
)
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuCheckboxItem = createDropdownMenuCheckedItem(
|
||||
DropdownMenuPrimitive.CheckboxItem,
|
||||
"dropdown-menu-checkbox-item",
|
||||
<CheckIcon className="size-4" />,
|
||||
"DropdownMenuCheckboxItem"
|
||||
)
|
||||
|
||||
const DropdownMenuRadioGroup = createDropdownMenuSlot(
|
||||
DropdownMenuPrimitive.RadioGroup,
|
||||
"dropdown-menu-radio-group",
|
||||
"DropdownMenuRadioGroup"
|
||||
)
|
||||
|
||||
const DropdownMenuRadioItem = createDropdownMenuCheckedItem(
|
||||
DropdownMenuPrimitive.RadioItem,
|
||||
"dropdown-menu-radio-item",
|
||||
<CircleIcon className="size-2 fill-current" />,
|
||||
"DropdownMenuRadioItem"
|
||||
)
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuSub = createDropdownMenuSlot(
|
||||
DropdownMenuPrimitive.Sub,
|
||||
"dropdown-menu-sub",
|
||||
"DropdownMenuSub"
|
||||
)
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
dropdownMenuContentMotionClass,
|
||||
"min-w-[8rem] overflow-hidden shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
23
product-source/hololake-platform/src/components/ui/input.tsx
Normal file
23
product-source/hololake-platform/src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { nativeTextAssistanceDisabledProps } from "@/lib/nativeTextAssistance"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
{...nativeTextAssistanceDisabledProps}
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { act, render, renderHook, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from './tooltip'
|
||||
import { useZoom } from '@/hooks/useZoom'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from './popover'
|
||||
|
||||
const PRESENCE_ANIMATION_CLASS_PARTS = [
|
||||
'animate-',
|
||||
'fade-',
|
||||
'zoom-in-',
|
||||
'zoom-out-',
|
||||
'slide-in-from',
|
||||
]
|
||||
|
||||
function expectNoPresenceAnimationClasses(element: HTMLElement) {
|
||||
const unstableClasses = element.className
|
||||
.split(/\s+/)
|
||||
.filter((className) =>
|
||||
PRESENCE_ANIMATION_CLASS_PARTS.some((part) => className.includes(part)),
|
||||
)
|
||||
|
||||
expect(unstableClasses).toEqual([])
|
||||
}
|
||||
|
||||
describe('overlay presence stability', () => {
|
||||
beforeEach(() => {
|
||||
document.documentElement.style.removeProperty('--tolaria-overlay-zoom-factor')
|
||||
document.documentElement.style.removeProperty('--tolaria-overlay-zoom-inverse')
|
||||
})
|
||||
|
||||
it('keeps tooltip content free of Radix presence animation classes', () => {
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button">Tooltip trigger</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent data-testid="tooltip-content">Tooltip copy</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
expectNoPresenceAnimationClasses(screen.getByTestId('tooltip-content'))
|
||||
})
|
||||
|
||||
it('publishes zoom variables for overlay portal positioning and visual scale', () => {
|
||||
const { result } = renderHook(() => useZoom())
|
||||
|
||||
act(() => {
|
||||
result.current.zoomIn()
|
||||
})
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue('--tolaria-overlay-zoom-factor')).toBe(String(110 / 100))
|
||||
expect(document.documentElement.style.getPropertyValue('--tolaria-overlay-zoom-inverse')).toBe(String(100 / 110))
|
||||
})
|
||||
|
||||
it('keeps tooltip positioning and arrow geometry in the same Radix shell', async () => {
|
||||
document.documentElement.style.setProperty('--tolaria-overlay-zoom-factor', '1.4')
|
||||
document.documentElement.style.setProperty('--tolaria-overlay-zoom-inverse', String(1 / 1.4))
|
||||
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<Tooltip open>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button">Tooltip trigger</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent data-testid="tooltip-content">Tooltip copy</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const positionShell = document.querySelector('[data-slot="tooltip-content"]') as HTMLElement
|
||||
const positionWrapper = positionShell.parentElement as HTMLElement
|
||||
expect(positionWrapper).toHaveAttribute('data-radix-popper-content-wrapper')
|
||||
await waitFor(() => {
|
||||
expect(positionWrapper.style.transform).toContain('translate')
|
||||
})
|
||||
expect(positionWrapper).not.toHaveAttribute('data-tolaria-tooltip-position-zoom')
|
||||
expect(positionWrapper.style.getPropertyValue('--tolaria-tooltip-wrapper-zoom')).toBe('')
|
||||
expect(positionWrapper.style.getPropertyValue('zoom')).toBe('')
|
||||
expect(positionShell.className).not.toContain('[zoom:var(--tolaria-overlay-zoom-inverse,1)]')
|
||||
expect(positionShell.className).not.toContain('[zoom:var(--tolaria-overlay-zoom-factor,1)]')
|
||||
expect(document.querySelector('[data-slot="tooltip-visual-scale"]')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps popover content free of Radix presence animation classes', () => {
|
||||
render(
|
||||
<Popover open>
|
||||
<PopoverTrigger asChild>
|
||||
<button type="button">Popover trigger</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent data-testid="popover-content">Popover copy</PopoverContent>
|
||||
</Popover>,
|
||||
)
|
||||
|
||||
expectNoPresenceAnimationClasses(screen.getByTestId('popover-content'))
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import * as React from "react"
|
||||
import { Popover as PopoverPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
collisionPadding = 8,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
collisionPadding={collisionPadding}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground z-[12000] w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
interface PopoverTextSlotProps extends React.HTMLAttributes<HTMLElement> {
|
||||
baseClassName: string
|
||||
element: "div" | "p"
|
||||
slot: string
|
||||
}
|
||||
|
||||
function PopoverTextSlot({
|
||||
baseClassName,
|
||||
className,
|
||||
element,
|
||||
slot,
|
||||
...props
|
||||
}: PopoverTextSlotProps) {
|
||||
return React.createElement(
|
||||
element,
|
||||
{
|
||||
"data-slot": slot,
|
||||
className: cn(baseClassName, className),
|
||||
...props,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverHeader(props: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<PopoverTextSlot
|
||||
baseClassName="flex flex-col gap-1 text-sm"
|
||||
element="div"
|
||||
slot="popover-header"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverTitle(props: React.ComponentProps<"div">) {
|
||||
return <PopoverTextSlot baseClassName="font-medium" element="div" slot="popover-title" {...props} />
|
||||
}
|
||||
|
||||
function PopoverDescription(props: React.ComponentProps<"p">) {
|
||||
return <PopoverTextSlot baseClassName="text-muted-foreground" element="p" slot="popover-description" {...props} />
|
||||
}
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverAnchor,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverDescription,
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react"
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
202
product-source/hololake-platform/src/components/ui/select.tsx
Normal file
202
product-source/hololake-platform/src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import * as React from "react"
|
||||
import { CaretDown as ChevronDownIcon, CaretUp as ChevronUpIcon, Check as CheckIcon } from '@phosphor-icons/react'
|
||||
import { Select as SelectPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function selectSlotProps(slot: string, baseClassName: string, className?: string) {
|
||||
return {
|
||||
"data-slot": slot,
|
||||
className: cn(baseClassName, className),
|
||||
}
|
||||
}
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
collisionPadding = 8,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-[12000] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-hidden rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
collisionPadding={collisionPadding}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectStaticPartProps =
|
||||
| (React.ComponentProps<typeof SelectPrimitive.Label> & { staticPart: "label" })
|
||||
| (React.ComponentProps<typeof SelectPrimitive.Separator> & { staticPart: "separator" })
|
||||
|
||||
function SelectStaticPart(props: SelectStaticPartProps) {
|
||||
if (props.staticPart === "label") {
|
||||
const { staticPart, className, ...labelProps } = props
|
||||
void staticPart
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
{...selectSlotProps("select-label", "text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...labelProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const { staticPart, className, ...separatorProps } = props
|
||||
void staticPart
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
{...selectSlotProps("select-separator", "bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...separatorProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel(props: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return <SelectStaticPart staticPart="label" {...props} />
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator(props: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return <SelectStaticPart staticPart="separator" {...props} />
|
||||
}
|
||||
|
||||
function SelectScrollButton({
|
||||
className,
|
||||
direction,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton> & {
|
||||
direction: "down" | "up"
|
||||
}) {
|
||||
const Primitive = direction === "up"
|
||||
? SelectPrimitive.ScrollUpButton
|
||||
: SelectPrimitive.ScrollDownButton
|
||||
const Icon = direction === "up" ? ChevronUpIcon : ChevronDownIcon
|
||||
const slot = direction === "up" ? "select-scroll-up-button" : "select-scroll-down-button"
|
||||
|
||||
return (
|
||||
<Primitive
|
||||
{...selectSlotProps(slot, "flex cursor-default items-center justify-center py-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</Primitive>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton(props: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return <SelectScrollButton direction="up" {...props} />
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return <SelectScrollButton direction="down" {...props} />
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SwitchProps extends Omit<React.ComponentProps<"button">, "onChange"> {
|
||||
checked?: boolean
|
||||
onCheckedChange?: (checked: boolean) => void
|
||||
}
|
||||
|
||||
function Switch({
|
||||
checked = false,
|
||||
className,
|
||||
onCheckedChange,
|
||||
onClick,
|
||||
type = "button",
|
||||
...props
|
||||
}: SwitchProps) {
|
||||
return (
|
||||
<button
|
||||
data-slot="switch"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
type={type}
|
||||
className={cn(
|
||||
"peer inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-colors outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
checked ? "bg-primary" : "bg-input",
|
||||
className,
|
||||
)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
if (!event.defaultPrevented) onCheckedChange?.(!checked)
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"pointer-events-none block size-4 rounded-full bg-background transition-transform",
|
||||
checked ? "translate-x-4" : "translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
90
product-source/hololake-platform/src/components/ui/tabs.tsx
Normal file
90
product-source/hololake-platform/src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
|
||||
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- shadcn/ui pattern
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Input } from './input'
|
||||
import { Textarea } from './textarea'
|
||||
|
||||
describe('text controls', () => {
|
||||
it('keeps inputs out of spellcheck without disabling IME autocorrection', () => {
|
||||
render(<Input aria-label="Search" />)
|
||||
|
||||
const input = screen.getByLabelText('Search')
|
||||
expect(input).toHaveAttribute('spellcheck', 'false')
|
||||
expect(input).toHaveAttribute('autocomplete', 'off')
|
||||
expect(input).not.toHaveAttribute('autocorrect')
|
||||
expect(input).not.toHaveAttribute('autocapitalize')
|
||||
})
|
||||
|
||||
it('keeps textareas out of spellcheck without disabling IME autocorrection', () => {
|
||||
render(<Textarea aria-label="Message" />)
|
||||
|
||||
const textarea = screen.getByLabelText('Message')
|
||||
expect(textarea).toHaveAttribute('spellcheck', 'false')
|
||||
expect(textarea).toHaveAttribute('autocomplete', 'off')
|
||||
expect(textarea).not.toHaveAttribute('autocorrect')
|
||||
expect(textarea).not.toHaveAttribute('autocapitalize')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react"
|
||||
|
||||
import { nativeTextAssistanceDisabledProps } from "@/lib/nativeTextAssistance"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<"textarea">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<textarea
|
||||
ref={ref}
|
||||
data-slot="textarea"
|
||||
{...nativeTextAssistanceDisabledProps}
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground flex min-h-20 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
collisionPadding = 8,
|
||||
children,
|
||||
style,
|
||||
...props
|
||||
}, forwardedRef) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={forwardedRef}
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
collisionPadding={collisionPadding}
|
||||
className={cn(
|
||||
"bg-foreground text-background z-50 w-fit max-w-[min(var(--radix-tooltip-content-available-width,22rem),22rem)] origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
})
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Loading…
Reference in a new issue