@anker-in/headless-ui 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/esm/components/badge.d.ts +9 -0
  2. package/dist/esm/components/button.d.ts +19 -0
  3. package/dist/esm/components/checkbox.d.ts +4 -0
  4. package/dist/esm/components/dialog.d.ts +19 -0
  5. package/dist/esm/components/heading.d.ts +7 -0
  6. package/dist/esm/components/index.d.ts +10 -0
  7. package/dist/esm/components/input.d.ts +21 -0
  8. package/dist/esm/components/popover.d.ts +6 -0
  9. package/dist/esm/components/radio.d.ts +5 -0
  10. package/dist/esm/components/skeleton.d.ts +6 -0
  11. package/dist/esm/components/text.d.ts +8 -0
  12. package/dist/esm/components/theme.d.ts +8 -0
  13. package/dist/esm/helpers/component-props.d.ts +7 -0
  14. package/dist/esm/helpers/constants.d.ts +2 -0
  15. package/dist/esm/helpers/index.d.ts +1 -0
  16. package/dist/esm/helpers/utils.d.ts +2 -0
  17. package/dist/esm/icons/spinner.d.ts +8 -0
  18. package/dist/esm/index.d.ts +1 -0
  19. package/dist/esm/index.js +42 -0
  20. package/dist/esm/index.js.map +7 -0
  21. package/dist/esm/stories/badge.stories.d.ts +26 -0
  22. package/dist/esm/stories/button.stories.d.ts +39 -0
  23. package/dist/esm/stories/checkbox.stories.d.ts +6 -0
  24. package/dist/esm/stories/dialog.stories.d.ts +6 -0
  25. package/dist/esm/stories/input.stories.d.ts +30 -0
  26. package/dist/esm/stories/popover.stories.d.ts +6 -0
  27. package/dist/esm/stories/radio.stories.d.ts +6 -0
  28. package/dist/esm/stories/skeleton.stories.d.ts +14 -0
  29. package/dist/style.css +1352 -0
  30. package/package.json +64 -0
  31. package/src/components/__tests__/button.test.js +9 -0
  32. package/src/components/badge.tsx +29 -0
  33. package/src/components/button.tsx +92 -0
  34. package/src/components/checkbox.tsx +26 -0
  35. package/src/components/dialog.tsx +97 -0
  36. package/src/components/heading.tsx +23 -0
  37. package/src/components/index.ts +15 -0
  38. package/src/components/input.tsx +73 -0
  39. package/src/components/popover.tsx +29 -0
  40. package/src/components/radio.tsx +36 -0
  41. package/src/components/skeleton.tsx +10 -0
  42. package/src/components/text.tsx +22 -0
  43. package/src/components/theme.tsx +17 -0
  44. package/src/helpers/component-props.ts +17 -0
  45. package/src/helpers/constants.ts +3 -0
  46. package/src/helpers/index.ts +1 -0
  47. package/src/helpers/utils.ts +6 -0
  48. package/src/icons/spinner.tsx +30 -0
  49. package/src/index.ts +1 -0
  50. package/src/stories/badge.stories.tsx +44 -0
  51. package/src/stories/button.stories.tsx +90 -0
  52. package/src/stories/checkbox.stories.tsx +16 -0
  53. package/src/stories/dialog.stories.tsx +45 -0
  54. package/src/stories/input.stories.tsx +59 -0
  55. package/src/stories/popover.stories.tsx +24 -0
  56. package/src/stories/radio.stories.tsx +19 -0
  57. package/src/stories/skeleton.stories.tsx +33 -0
  58. package/src/styles/global.css +196 -0
  59. package/src/styles/index.css +0 -0
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@anker-in/headless-ui",
3
+ "version": "0.0.2",
4
+ "main": "./dist/esm/index.js",
5
+ "types": "./dist/esm/index.d.ts",
6
+ "module": "./dist/esm/index.js",
7
+ "style": "./dist/style.css",
8
+ "description": "",
9
+ "type": "module",
10
+ "files": [
11
+ "src/**",
12
+ "dist/**"
13
+ ],
14
+ "sideEffects": false,
15
+ "keywords": [],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "devDependencies": {
19
+ "@babel/preset-env": "^7.24.3",
20
+ "@babel/preset-react": "^7.24.1",
21
+ "@babel/preset-typescript": "^7.24.1",
22
+ "@testing-library/react": "^14.2.2",
23
+ "babel-jest": "^29.7.0",
24
+ "esbuild": "^0.20.2",
25
+ "esbuild-style-plugin": "^1.6.3",
26
+ "eslint-plugin-require-extensions": "^0.1.3",
27
+ "jest": "^29.7.0",
28
+ "jest-environment-jsdom": "^29.7.0",
29
+ "postcss-cli": "^11.0.0",
30
+ "react-test-renderer": "^18.2.0",
31
+ "stylelint": "^16.2.1"
32
+ },
33
+ "dependencies": {
34
+ "@radix-ui/react-checkbox": "^1.0.4",
35
+ "@radix-ui/react-dialog": "^1.0.5",
36
+ "@radix-ui/react-icons": "^1.3.0",
37
+ "@radix-ui/react-popover": "^1.0.7",
38
+ "@radix-ui/react-radio-group": "^1.1.3",
39
+ "@radix-ui/react-slot": "^1.0.2",
40
+ "@radix-ui/react-visually-hidden": "^1.0.3",
41
+ "autoprefixer": "^10.4.18",
42
+ "class-variance-authority": "^0.7.0",
43
+ "clsx": "^2.1.0",
44
+ "postcss": "^8.4.37",
45
+ "tailwind-merge": "^2.2.2",
46
+ "tailwindcss": "^3.4.1"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "registry": "https://registry.npmjs.org/"
51
+ },
52
+ "scripts": {
53
+ "dev": "pnpm run dev:js:esm & pnpm run dev:js:esm:types & pnpm run dev:css",
54
+ "dev:js:esm": "node build.esm.js watch=true",
55
+ "dev:js:esm:types": "tsc --watch --incremental --outdir dist/esm >/dev/null",
56
+ "dev:css": "postcss src/styles/global.css -o dist/style.css --watch",
57
+ "build": "pnpm run build:css & pnpm run build:js:esm & pnpm run build:js:esm:types ",
58
+ "build:js:esm": "node build.esm.js",
59
+ "build:js:esm:types": "tsc --outdir dist/esm",
60
+ "build:css": "postcss src/styles/global.css -o dist/style.css",
61
+ "lint": "eslint \"src/**/*.ts*\"",
62
+ "test": "jest"
63
+ }
64
+ }
@@ -0,0 +1,9 @@
1
+ import { cleanup, fireEvent, render } from '@testing-library/react'
2
+ import Button from '../button'
3
+
4
+ test('button click should call the onClick handler', () => {
5
+ const onClick = jest.fn()
6
+ const { getByText } = render(<Button onClick={onClick}>Click me</Button>)
7
+ fireEvent.click(getByText('Click me'))
8
+ expect(onClick).toHaveBeenCalled()
9
+ })
@@ -0,0 +1,29 @@
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+
4
+ import { cn } from '../helpers/index'
5
+
6
+ const badgeVariants = cva(
7
+ 'inline-flex items-center rounded-[20px] border px-[10px] py-1 text-xs font-medium transition-colors focus:outline-none',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-badge text-badge-foreground border-transparent',
12
+ secondary: 'bg-secondary text-secondary-foreground border-transparent',
13
+ destructive: 'bg-destructive text-destructive-foreground border-transparent',
14
+ outline: 'text-foreground',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'default',
19
+ },
20
+ }
21
+ )
22
+
23
+ export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
24
+
25
+ function Badge({ className, variant, ...props }: BadgeProps) {
26
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />
27
+ }
28
+
29
+ export default Badge
@@ -0,0 +1,92 @@
1
+ import React from 'react'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
5
+ import { cn } from '../helpers/index'
6
+ import Spinner from '../icons/spinner'
7
+
8
+ /**
9
+ * Button Variants
10
+ */
11
+ const buttonVariants = cva(
12
+ 'ring-offset-background focus-visible:ring-ring rounded-btn inline-flex items-center justify-center whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
13
+ {
14
+ variants: {
15
+ variant: {
16
+ primary:
17
+ 'bg-btn-primary text-btn-primary-foreground hover:bg-btn-primary-active disabled:bg-btn-primary-disabled disabled:text-btn-primary-disabled-foreground',
18
+ secondary:
19
+ 'bg-btn-secondary border-primary text-btn-secondary-foreground hover:bg-btn-secondary-active hover:text-btn-secondary disabled:bg-btn-secondary-disabled disabled:text-btn-secondary-disabled-foreground border border-solid',
20
+ link: 'text-primary underline-offset-4 hover:underline',
21
+ ghost: 'hover:bg-btn-primary hover:text-btn-primary-foreground',
22
+ },
23
+ size: {
24
+ sm: 'h-8 px-3 py-1',
25
+ base: 'h-10 px-4 py-2',
26
+ lg: 'h-11 px-8',
27
+ icon: 'size-10 rounded-full',
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: 'primary',
32
+ size: 'base',
33
+ },
34
+ }
35
+ )
36
+
37
+ export interface ButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof buttonVariants> {
40
+ /** 设置按钮载入状态 */
41
+ loading?: boolean
42
+ /** 设置按钮失效状态 */
43
+ disabled?: boolean
44
+ /** 定义自组件作为父组件的类型, 详细使用方式请参考radix-ui */
45
+ asChild?: boolean
46
+ }
47
+
48
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
49
+ (
50
+ {
51
+ asChild = false,
52
+ size = 'base',
53
+ children,
54
+ variant,
55
+ className,
56
+ disabled = false,
57
+ loading = false,
58
+ ...props
59
+ }: ButtonProps,
60
+ ref
61
+ ) => {
62
+ const Comp = asChild ? Slot : 'button'
63
+
64
+ return (
65
+ <Comp
66
+ disabled={disabled}
67
+ className={cn(buttonVariants({ variant, size, className }), {
68
+ 'cursor-not-allowed': disabled,
69
+ })}
70
+ ref={ref}
71
+ {...props}
72
+ >
73
+ {loading ? (
74
+ <>
75
+ <span style={{ display: 'contents', visibility: 'hidden' }} aria-hidden>
76
+ {children}
77
+ </span>
78
+ <span className="absolute">
79
+ <Spinner />
80
+ </span>
81
+ <VisuallyHidden>{children}</VisuallyHidden>
82
+ </>
83
+ ) : (
84
+ children
85
+ )}
86
+ </Comp>
87
+ )
88
+ }
89
+ )
90
+
91
+ Button.displayName = 'Button'
92
+ export default Button
@@ -0,0 +1,26 @@
1
+ import * as React from 'react'
2
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
3
+ import { CheckIcon } from '@radix-ui/react-icons'
4
+
5
+ import { cn } from '../helpers/index'
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ 'border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer size-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
15
+ className
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}>
20
+ <CheckIcon className="size-4" />
21
+ </CheckboxPrimitive.Indicator>
22
+ </CheckboxPrimitive.Root>
23
+ ))
24
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName
25
+
26
+ export default Checkbox
@@ -0,0 +1,97 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
5
+ import { Cross2Icon } from '@radix-ui/react-icons'
6
+
7
+ import { cn } from '../helpers/index'
8
+
9
+ const Dialog = DialogPrimitive.Root
10
+
11
+ const DialogTrigger = DialogPrimitive.Trigger
12
+
13
+ const DialogPortal = DialogPrimitive.Portal
14
+
15
+ const DialogClose = DialogPrimitive.Close
16
+
17
+ const DialogOverlay = React.forwardRef<
18
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
19
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
+ >(({ className, ...props }, ref) => (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ '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/80',
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ ))
30
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
+
32
+ const DialogContent = React.forwardRef<
33
+ React.ElementRef<typeof DialogPrimitive.Content>,
34
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
+ >(({ className, children, ...props }, ref) => (
36
+ <DialogPortal>
37
+ <DialogOverlay />
38
+ <DialogPrimitive.Content
39
+ ref={ref}
40
+ className={cn(
41
+ '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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
42
+ className
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
48
+ <Cross2Icon className="size-4" />
49
+ <span className="sr-only">Close</span>
50
+ </DialogPrimitive.Close>
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ ))
54
+ DialogContent.displayName = DialogPrimitive.Content.displayName
55
+
56
+ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
57
+ <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
58
+ )
59
+ DialogHeader.displayName = 'DialogHeader'
60
+
61
+ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
62
+ <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
63
+ )
64
+ DialogFooter.displayName = 'DialogFooter'
65
+
66
+ const DialogTitle = React.forwardRef<
67
+ React.ElementRef<typeof DialogPrimitive.Title>,
68
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
69
+ >(({ className, ...props }, ref) => (
70
+ <DialogPrimitive.Title
71
+ ref={ref}
72
+ className={cn('text-lg font-semibold leading-none tracking-tight', className)}
73
+ {...props}
74
+ />
75
+ ))
76
+ DialogTitle.displayName = DialogPrimitive.Title.displayName
77
+
78
+ const DialogDescription = React.forwardRef<
79
+ React.ElementRef<typeof DialogPrimitive.Description>,
80
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
81
+ >(({ className, ...props }, ref) => (
82
+ <DialogPrimitive.Description ref={ref} className={cn('text-muted-foreground text-sm', className)} {...props} />
83
+ ))
84
+ DialogDescription.displayName = DialogPrimitive.Description.displayName
85
+
86
+ export {
87
+ Dialog,
88
+ DialogPortal,
89
+ DialogOverlay,
90
+ DialogClose,
91
+ DialogTrigger,
92
+ DialogContent,
93
+ DialogHeader,
94
+ DialogFooter,
95
+ DialogTitle,
96
+ DialogDescription,
97
+ }
@@ -0,0 +1,23 @@
1
+ import { cn } from '@/helpers'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import React from 'react'
4
+
5
+ type HeadingElement = React.ElementRef<'h1'>
6
+
7
+ interface HeadingProps extends React.ComponentPropsWithoutRef<'h1'> {
8
+ as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | undefined
9
+ asChild?: boolean
10
+ }
11
+
12
+ const Heading = React.forwardRef<HeadingElement, HeadingProps>((props, forwardedRef) => {
13
+ const { children, className, asChild, as: Tag = 'h1', color, ...headingProps } = props
14
+ return (
15
+ <Slot data-accent-color={color || undefined} {...headingProps} ref={forwardedRef} className={cn('', className)}>
16
+ {asChild ? children : <Tag>{children}</Tag>}
17
+ </Slot>
18
+ )
19
+ })
20
+
21
+ Heading.displayName = 'Heading'
22
+
23
+ export { Heading }
@@ -0,0 +1,15 @@
1
+ // theme
2
+ export { default as Theme } from './theme'
3
+
4
+ // components
5
+ export { default as Button } from './button'
6
+ export { default as Badge } from './badge'
7
+ export * from './input'
8
+
9
+ export { default as Checkbox } from './checkbox'
10
+
11
+ export { default as Skeleton } from './skeleton'
12
+ export * from './dialog'
13
+ export * from './popover'
14
+ export * from './radio'
15
+ export * from './dialog'
@@ -0,0 +1,73 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../helpers/index'
3
+ import { ComponentPropsWithout, RemovedProps } from '@/helpers/component-props'
4
+ import { cva, type VariantProps } from 'class-variance-authority'
5
+
6
+ const inputVariants = cva(
7
+ 'border-input bg-background focus-visible:ring-ring ring-offset-background flex h-2 flex-row items-center overflow-hidden rounded-md border px-3 focus-visible:ring-2 focus-visible:ring-offset-2',
8
+ {
9
+ variants: {
10
+ size: {
11
+ sm: 'h-8',
12
+ base: 'h-10',
13
+ lg: 'h-12',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ size: 'base',
18
+ },
19
+ }
20
+ )
21
+
22
+ export interface InputProps extends ComponentPropsWithout<'input', 'size'>, VariantProps<typeof inputVariants> {}
23
+
24
+ type InputSlotElement = React.ElementRef<'div'>
25
+ interface InputSlotProps extends ComponentPropsWithout<'div', RemovedProps> {
26
+ /**
27
+ * 插槽的位置
28
+ */
29
+ side: 'left' | 'right'
30
+ }
31
+
32
+ /**
33
+ * InputSlot
34
+ * 用来在 Input 组件中插入额外的内容,比如icon ,按钮
35
+ */
36
+ const InputSlot = React.forwardRef<InputSlotElement, InputSlotProps>((props, forwardedRef) => {
37
+ const { className, side, children, ...slotProps } = props
38
+
39
+ return (
40
+ <div
41
+ ref={forwardedRef}
42
+ {...slotProps}
43
+ className={cn('flex h-full items-center px-2', className, {
44
+ '-order-1 pl-0': side === 'left',
45
+ 'order-2 pr-0': side === 'right',
46
+ })}
47
+ >
48
+ {children}
49
+ </div>
50
+ )
51
+ })
52
+
53
+ const Input = React.forwardRef<React.ElementRef<'input'>, InputProps>(
54
+ ({ className, type, size, children, ...props }, ref) => {
55
+ return (
56
+ <div className={cn(inputVariants({ size }), className)}>
57
+ <input
58
+ type={type}
59
+ className={cn(
60
+ 'placeholder:text-muted-foreground flex size-full text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50'
61
+ )}
62
+ ref={ref}
63
+ {...props}
64
+ />
65
+ {children}
66
+ </div>
67
+ )
68
+ }
69
+ )
70
+ Input.displayName = 'Input'
71
+ InputSlot.displayName = 'InputSlot'
72
+
73
+ export { Input, InputSlot }
@@ -0,0 +1,29 @@
1
+ import * as React from 'react'
2
+ import * as PopoverPrimitive from '@radix-ui/react-popover'
3
+
4
+ import { cn } from '../helpers/utils'
5
+
6
+ const Popover = PopoverPrimitive.Root
7
+
8
+ const PopoverTrigger = PopoverPrimitive.Trigger
9
+
10
+ const PopoverContent = React.forwardRef<
11
+ React.ElementRef<typeof PopoverPrimitive.Content>,
12
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
13
+ >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
14
+ <PopoverPrimitive.Portal>
15
+ <PopoverPrimitive.Content
16
+ ref={ref}
17
+ align={align}
18
+ sideOffset={sideOffset}
19
+ className={cn(
20
+ '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 w-72 rounded-md border p-4 shadow-md outline-none',
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ </PopoverPrimitive.Portal>
26
+ ))
27
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName
28
+
29
+ export { Popover, PopoverTrigger, PopoverContent }
@@ -0,0 +1,36 @@
1
+ import * as React from 'react'
2
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
3
+ import { CheckIcon } from '@radix-ui/react-icons'
4
+
5
+ import { cn } from '../helpers/index'
6
+
7
+ const RadioGroup = React.forwardRef<
8
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
10
+ >(({ className, ...props }, ref) => {
11
+ return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />
12
+ })
13
+ RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
14
+
15
+ const RadioGroupItem = React.forwardRef<
16
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
17
+ React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
18
+ >(({ className, ...props }, ref) => {
19
+ return (
20
+ <RadioGroupPrimitive.Item
21
+ ref={ref}
22
+ className={cn(
23
+ 'border-primary text-primary ring-offset-background focus-visible:ring-ring aspect-square size-4 rounded-full border focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
24
+ className
25
+ )}
26
+ {...props}
27
+ >
28
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
29
+ <CheckIcon className="text-primary size-2.5 fill-current" />
30
+ </RadioGroupPrimitive.Indicator>
31
+ </RadioGroupPrimitive.Item>
32
+ )
33
+ })
34
+ RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
35
+
36
+ export { RadioGroup, RadioGroupItem }
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import { cn } from '../helpers/index'
3
+
4
+ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
5
+ return <div className={cn('bg-skeleton size-12 animate-pulse', className)} {...props} />
6
+ }
7
+
8
+ Skeleton.displayName = 'Skeleton'
9
+
10
+ export default Skeleton
@@ -0,0 +1,22 @@
1
+ import { cn } from '@/helpers'
2
+ import { Slot } from '@radix-ui/react-slot'
3
+ import React from 'react'
4
+
5
+ type TextElement = React.ElementRef<'span'>
6
+ interface TextProps extends React.ComponentPropsWithoutRef<'span'> {
7
+ as?: 'div' | 'label' | 'p' | 'span' | undefined
8
+ asChild?: boolean
9
+ color?: string
10
+ }
11
+
12
+ const Text = React.forwardRef<TextElement, TextProps>((props, forwardedRef) => {
13
+ const { children, className, asChild, as: Tag = 'span', color, ...textProps } = props
14
+ return (
15
+ <Slot {...textProps} ref={forwardedRef} className={cn(className, color)}>
16
+ {asChild ? children : <Tag>{children}</Tag>}
17
+ </Slot>
18
+ )
19
+ })
20
+ Text.displayName = 'Text'
21
+
22
+ export { Text }
@@ -0,0 +1,17 @@
1
+ import type { Brand } from '@/helpers/constants'
2
+ import React, { useEffect } from 'react'
3
+
4
+ interface ThemeProps {
5
+ brand: Brand
6
+ children: React.ReactNode
7
+ }
8
+ const Theme = ({ brand, children }: ThemeProps) => {
9
+ useEffect(() => {
10
+ // TODO: 在ssr 中不生效, 暂时手动在外层设置
11
+ document.getElementsByTagName('html')?.[0]?.setAttribute('data-brand-theme', brand)
12
+ }, [brand])
13
+
14
+ return <>{children}</>
15
+ }
16
+
17
+ export default Theme
@@ -0,0 +1,17 @@
1
+ import type React from 'react'
2
+
3
+ type ComponentPropsAs<C extends React.ElementType<any>, T extends React.ComponentPropsWithoutRef<C>['as']> = Omit<
4
+ Extract<React.ComponentPropsWithoutRef<C>, { as: T }>,
5
+ 'as' | 'asChild'
6
+ >
7
+
8
+ // Omits the specified props from the component props. Autocomplete will suggest props
9
+ // of the component, but won't restrict the omittable props to those that actually exist.
10
+ type ComponentPropsWithout<
11
+ T extends React.ElementType,
12
+ O extends Omit<string, keyof React.ComponentPropsWithoutRef<T>> | keyof React.ComponentPropsWithoutRef<T>,
13
+ > = Omit<React.ComponentPropsWithoutRef<T>, O & string>
14
+
15
+ type RemovedProps = 'asChild' | 'defaultChecked' | 'defaultValue' | 'color'
16
+
17
+ export type { ComponentPropsAs, ComponentPropsWithout, RemovedProps }
@@ -0,0 +1,3 @@
1
+ export const ALL_BRANDS = ['anker', 'eufy', 'soundcore', 'nebula', 'ankerWork', 'ankerMake'] as const
2
+
3
+ export type Brand = (typeof ALL_BRANDS)[number]
@@ -0,0 +1 @@
1
+ export { cn } from './utils'
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+
3
+ export interface SpinnerProps {
4
+ /** loading 图标大小 */
5
+ size?: 'sm' | 'md' | 'lg'
6
+ color?: string
7
+ }
8
+
9
+ // eslint-disable-next-line no-unused-vars
10
+ const Spinner = ({ size = 'sm', color = '#ffffff' }: SpinnerProps) => {
11
+ return (
12
+ <svg
13
+ className="animate-spin"
14
+ width="20"
15
+ height="20"
16
+ viewBox="0 0 20 20"
17
+ fill="none"
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ >
20
+ <path
21
+ d="M3 10C3 13.866 6.13401 17 10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3"
22
+ stroke={color}
23
+ strokeWidth="1.6"
24
+ strokeLinecap="round"
25
+ />
26
+ </svg>
27
+ )
28
+ }
29
+
30
+ export default Spinner
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './components/index'
@@ -0,0 +1,44 @@
1
+ import type { Meta, StoryObj } from '@storybook/react'
2
+
3
+ import { Badge } from '../components/index'
4
+ import React from 'react'
5
+
6
+ const meta = {
7
+ title: 'Components/Badge',
8
+ component: Badge,
9
+ parameters: {
10
+ layout: 'centered',
11
+ },
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ variant: {
15
+ control: {
16
+ type: 'select',
17
+ },
18
+ options: ['default', 'destructive', 'outline', 'secondary'],
19
+ },
20
+ },
21
+ args: {
22
+ variant: 'default',
23
+ children: 'New',
24
+ },
25
+ } satisfies Meta<typeof Badge>
26
+
27
+ export default meta
28
+
29
+ type Story = StoryObj<typeof meta>
30
+
31
+ export const Default: Story = {}
32
+
33
+ export const Examples: Story = {
34
+ render() {
35
+ return (
36
+ <div className="flex flex-col space-y-4">
37
+ <Badge variant="default">Default</Badge>
38
+ <Badge variant="secondary">Secondary</Badge>
39
+ <Badge variant="destructive">Destructive</Badge>
40
+ <Badge variant="outline">Outline</Badge>
41
+ </div>
42
+ )
43
+ },
44
+ }