Skip to content

Commit 67af3aa

Browse files
committed
implement new code component
1 parent 4ef26a2 commit 67af3aa

File tree

5 files changed

+109
-10
lines changed

5 files changed

+109
-10
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { CheckIcon, CopyIcon } from './icon';
2+
import cn from 'clsx'
3+
import type { ComponentProps, FC } from 'react'
4+
import { useHover } from '@/lib/hooks/use-hover';
5+
import { useTimed } from '@/lib/hooks/use-timed';
6+
7+
export const Code: FC<
8+
ComponentProps<'code'>
9+
> = ({ children, className, ...props }) => {
10+
const [copied, startCopyTimer] = useTimed(1500);
11+
const [ref, hovering] = useHover();
12+
return (
13+
<span
14+
ref={ref}
15+
className="flex items-center gap-2 break-all rounded-md bg-black p-4 font-mono text-sm relative pr-14 border border-gray-600"
16+
>
17+
<code
18+
className={cn(
19+
'whitespace-pre-line',
20+
'cursor-text',
21+
className,
22+
)}
23+
// always show code blocks in ltr
24+
dir="ltr"
25+
{...props}
26+
>
27+
{children}
28+
</code>
29+
<button
30+
data-hovering={hovering || copied}
31+
className="cursor-pointer opacity-0 data-[hovering=true]:transition-opacity data-[hovering=true]:opacity-100 hover:text-orange-600 absolute right-3 top-2 p-2 border border-gray-600 rounded-md"
32+
onClick={async (ev) => {
33+
const value = children?.valueOf().toString();
34+
if (value) {
35+
ev.preventDefault();
36+
startCopyTimer();
37+
await navigator.clipboard.writeText(value);
38+
}
39+
}}
40+
title="Copy to clipboard"
41+
>
42+
{copied ? <CheckIcon size={16} /> : <CopyIcon size={16} />}
43+
</button>
44+
</span>
45+
)
46+
}

packages/web/app/src/components/ui/empty-list.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { ReactElement, ReactNode } from 'react';
22
import magnifier from '../../../public/images/figures/magnifier.svg?url';
33
import { ProjectType } from '@/gql/graphql';
44
import { cn } from '@/lib/utils';
5-
import { InlineCode } from '../v2/inline-code';
65
import { Card } from './card';
76
import { DocsLink } from './docs-note';
87
import { Heading } from './heading';
8+
import { Code } from './code';
99

1010
export const EmptyList = ({
1111
title,
@@ -68,9 +68,9 @@ export const NoSchemaVersion = ({
6868
registry before publishing.
6969
</div>
7070
<div className="flex w-full justify-center">
71-
<InlineCode
72-
content={`hive schema:check ${isDistributed ? '--service <service-name> --url <url> ' : ''}<path/schema.graphql>`}
73-
/>
71+
<Code>
72+
{`hive schema:check ${isDistributed ? '--service <service-name> --url <url> ' : ''}<path/schema.graphql>`}
73+
</Code>
7474
</div>
7575
</>
7676
);
@@ -84,9 +84,9 @@ export const NoSchemaVersion = ({
8484
</div>
8585
)}
8686
<div className="flex w-full justify-center">
87-
<InlineCode
88-
content={`hive schema:publish ${isDistributed ? '--service <service-name> --url <url> ' : ''}<path/schema.graphql>`}
89-
/>
87+
<Code>
88+
{`hive schema:publish ${isDistributed ? '--service <service-name> --url <url> ' : ''}<path/schema.graphql>`}
89+
</Code>
9090
</div>
9191
</>
9292
);

packages/web/app/src/components/ui/icon.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,11 @@ export const ArrowDownIcon = ({ className }: IconProps): ReactElement => (
231231
</svg>
232232
);
233233

234-
export const CheckIcon = ({ className }: IconProps): ReactElement => (
234+
export const CheckIcon = ({ className, size }: IconProps & { size?: number }): ReactElement => (
235235
<svg
236236
viewBox="0 0 24 24"
237-
width="24"
238-
height="24"
237+
width={size ?? 24}
238+
height={size ?? 24}
239239
stroke="currentColor"
240240
xmlns="http://www.w3.org/2000/svg"
241241
className={className}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useState, useCallback, useRef } from 'react';
2+
3+
export function useHover() {
4+
const [hovering, setHovering] = useState(false);
5+
const previousNode = useRef<Node | null>(null);
6+
7+
const handleMouseEnter = useCallback(() => {
8+
setHovering(true);
9+
}, []);
10+
11+
const handleMouseLeave = useCallback(() => {
12+
setHovering(false);
13+
}, []);
14+
15+
const customRef = useCallback(
16+
(node: HTMLElement) => {
17+
if (previousNode.current?.nodeType === Node.ELEMENT_NODE) {
18+
previousNode.current.removeEventListener(
19+
'mouseenter',
20+
handleMouseEnter
21+
);
22+
previousNode.current.removeEventListener(
23+
'mouseleave',
24+
handleMouseLeave
25+
);
26+
}
27+
28+
if (node?.nodeType === Node.ELEMENT_NODE) {
29+
node.addEventListener('mouseenter', handleMouseEnter);
30+
node.addEventListener('mouseleave', handleMouseLeave);
31+
}
32+
33+
previousNode.current = node;
34+
},
35+
[handleMouseEnter, handleMouseLeave]
36+
);
37+
38+
return [customRef, hovering] as const;
39+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { useState } from 'react';
2+
3+
export function useTimed(wait: number = 1000) {
4+
const [timer, setTimer] = useState<NodeJS.Timeout | null>(null);
5+
const handler = () => {
6+
if (timer) {
7+
clearTimeout(timer);
8+
}
9+
setTimer(setTimeout(() => {
10+
setTimer(null)
11+
}, wait));
12+
}
13+
return [timer !== null, handler] as const;
14+
}

0 commit comments

Comments
 (0)