-
Notifications
You must be signed in to change notification settings - Fork 716
refactor(dc-ui): extract DcCopyButton and wire copy-text #2131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
42cf8d3
7df856b
dd4f102
557b875
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| <script lang="ts"> | ||
| import { computed, defineComponent, h, onUnmounted, ref, type PropType } from 'vue' | ||
| import { useClipboard } from '@vueuse/core' | ||
| import { cn } from '@shadcn/lib/utils' | ||
| import { type DcButtonProps } from './props' | ||
| import DcButton from './DcButton.vue' | ||
|
|
||
| interface DcCopyButtonProps extends DcButtonProps { | ||
| copyText?: string | ||
| } | ||
|
|
||
| export default defineComponent({ | ||
| name: 'DcCopyButton', | ||
| inheritAttrs: false, | ||
| props: { | ||
| copyText: String, | ||
| variant: { | ||
| type: String as PropType<DcCopyButtonProps['variant']>, | ||
| default: 'ghost' | ||
| } | ||
| }, | ||
| emits: { | ||
| copied: () => true, | ||
| error: (_error: unknown) => true | ||
| }, | ||
| setup(props, { attrs, emit, slots }) { | ||
| const copied = ref(false) | ||
| let timer: ReturnType<typeof setTimeout> | undefined | ||
| const { copy } = useClipboard() | ||
| const icon = computed(() => (copied.value ? 'lucide:check' : 'lucide:copy')) | ||
|
|
||
| const copyText = async () => { | ||
| if (!props.copyText) return | ||
| try { | ||
| await copy(props.copyText) | ||
| copied.value = true | ||
| clearTimeout(timer) | ||
| timer = setTimeout(() => { | ||
| copied.value = false | ||
| }, 1200) | ||
| emit('copied') | ||
| } catch (error) { | ||
| console.error('[DcCopyButton] Failed to copy', error) | ||
| emit('error', error) | ||
| } | ||
|
zhangmo8 marked this conversation as resolved.
|
||
| } | ||
|
|
||
| onUnmounted(() => clearTimeout(timer)) | ||
|
|
||
| return () => { | ||
| const inheritedAttrs = attrs as Partial<DcButtonProps> & Record<string, unknown> | ||
| const { copyText: _copyText, ...buttonProps } = props | ||
| const accessibleName = inheritedAttrs.label ?? inheritedAttrs.tooltip ?? _copyText | ||
|
|
||
| return h( | ||
| DcButton, | ||
| { | ||
| ...inheritedAttrs, | ||
| ...buttonProps, | ||
| key: icon.value, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Keep keyboard focus while changing the success icon Using |
||
| icon: icon.value, | ||
| iconClass: cn( | ||
| inheritedAttrs.iconClass, | ||
| copied.value && 'animate-in zoom-in-75 duration-200' | ||
| ), | ||
| label: accessibleName, | ||
| class: cn( | ||
| 'shrink-0', | ||
| copied.value ? 'text-emerald-600 dark:text-emerald-400' : '', | ||
| inheritedAttrs.class | ||
| ), | ||
| onClick: copyText | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| }, | ||
| slots | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Preserve the visible label fallback The previous template rendered |
||
| ) | ||
| } | ||
| } | ||
| }) | ||
| </script> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,38 +1,3 @@ | ||
| import type { VariantProps } from 'class-variance-authority' | ||
| import { cva } from 'class-variance-authority' | ||
|
|
||
| export { default as DcButton } from './DcButton.vue' | ||
|
|
||
| export const dcButtonVariants = cva( | ||
| 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,scale] duration-[var(--dc-motion-fast)] ease-[var(--dc-ease-out-soft)] active:scale-[0.97] motion-reduce:active:scale-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 [&_svg]:shrink-0 shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', | ||
| { | ||
| variants: { | ||
| variant: { | ||
| default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', | ||
| destructive: | ||
| 'bg-destructive text-white shadow-xs 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 shadow-xs 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 gap-2 px-4 py-2 has-[>svg]:px-3', | ||
| sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', | ||
| xs: 'h-7 gap-1.5 rounded-md px-2.5 text-xs has-[>svg]:px-2', | ||
| lg: 'h-10 gap-2 rounded-md px-6 has-[>svg]:px-4', | ||
| icon: 'size-8', | ||
| 'icon-sm': 'size-7', | ||
| 'icon-xs': 'size-6', | ||
| 'icon-lg': 'size-10' | ||
| } | ||
| }, | ||
| defaultVariants: { | ||
| variant: 'default', | ||
| size: 'default' | ||
| } | ||
| } | ||
| ) | ||
|
|
||
| export type DcButtonVariants = VariantProps<typeof dcButtonVariants> | ||
| export { default as DcCopyButton } from './DcCopyButton.vue' | ||
| export * from './props' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import type { PrimitiveProps } from 'reka-ui' | ||
| import type { HTMLAttributes } from 'vue' | ||
|
|
||
| export interface DcButtonProps extends PrimitiveProps { | ||
| variant?: DcButtonVariants['variant'] | ||
| size?: DcButtonVariants['size'] | ||
| icon?: string | ||
| iconSize?: DcIconSize | ||
| iconClass?: HTMLAttributes['class'] | ||
| loading?: boolean | ||
| disabled?: boolean | ||
| active?: boolean | ||
| /** Visible tooltip. `label` alone only provides the accessible name. */ | ||
| tooltip?: string | ||
| tooltipSide?: DcTooltipSide | ||
| tooltipSideOffset?: number | ||
| tooltipDelayDuration?: number | ||
| tooltipContentClass?: HTMLAttributes['class'] | ||
| tooltipIgnoreNonKeyboardFocus?: boolean | ||
| label?: string | ||
| class?: HTMLAttributes['class'] | ||
| } | ||
|
|
||
| import type { VariantProps } from 'class-variance-authority' | ||
| import { cva } from 'class-variance-authority' | ||
|
|
||
| type DcIconSize = '3' | '3.5' | '4' | ||
| type DcTooltipSide = 'top' | 'bottom' | 'left' | 'right' | ||
|
|
||
| export const dcButtonVariants = cva( | ||
| 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-[color,background-color,border-color,scale] duration-[var(--dc-motion-fast)] ease-[var(--dc-ease-out-soft)] active:scale-[0.97] motion-reduce:active:scale-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 [&_svg]:shrink-0 shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', | ||
| { | ||
| variants: { | ||
| variant: { | ||
| default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', | ||
| destructive: | ||
| 'bg-destructive text-white shadow-xs 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 shadow-xs 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 gap-2 px-4 py-2 has-[>svg]:px-3', | ||
| sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', | ||
| xs: 'h-7 gap-1.5 rounded-md px-2.5 text-xs has-[>svg]:px-2', | ||
| lg: 'h-10 gap-2 rounded-md px-6 has-[>svg]:px-4', | ||
| icon: 'size-8', | ||
| 'icon-sm': 'size-7', | ||
| 'icon-xs': 'size-6', | ||
| 'icon-lg': 'size-10' | ||
| } | ||
| }, | ||
| defaultVariants: { | ||
| variant: 'default', | ||
| size: 'default' | ||
| } | ||
| } | ||
| ) | ||
|
|
||
| export type DcButtonVariants = VariantProps<typeof dcButtonVariants> |
Uh oh!
There was an error while loading. Please reload this page.