Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions operator/validators-manager.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description: Set up the Validators Manager role for your StakeWise Vault. Create
---

import Image from '@theme/IdealImage'
import { CommandSnippet } from '@site/src/components'

# Validators Manager

Expand Down Expand Up @@ -42,9 +43,7 @@ You may reuse the same mnemonic as your validator keys or generate a separate on

Export a private key from an existing wallet, then set it as an environment variable:

```bash
export WALLET_PRIVATE_KEY="0xyour_private_key_here"
```
<CommandSnippet template={`export WALLET_PRIVATE_KEY="{{0xyour_private_key_here}}"`} />

If both `WALLET_PRIVATE_KEY` and wallet keystore files are configured, the private key takes precedence over the wallet files.

Expand Down
127 changes: 127 additions & 0 deletions src/components/CommandSnippet/CommandSnippet.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
@use 'mixins' as *;

.snippet {
position: relative;
margin-bottom: var(--ifm-leading);
}

.pre {
margin: 0;
padding: 0.75rem 1rem;
overflow-x: auto;
--ifm-pre-padding: 1rem;
background: var(--prism-background-color, var(--ifm-code-background));
border-radius: var(--ifm-code-border-radius);
font-size: var(--ifm-code-font-size);
}

.code {
display: inline-flex;
align-items: center;
white-space: pre;
font-family: var(--ifm-font-family-monospace);
line-height: var(--ifm-pre-line-height);
}

.field {
font-family: inherit;
font-size: inherit;
line-height: inherit;
color: inherit;
padding: 0.5rem 1.5rem 0.5rem 1rem;
margin: 0 0.35rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 20h9'/%3E%3Cpath d='M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.4rem center;
background-size: 0.85em 0.85em;
border-radius: 4px;
outline: none;
min-width: 1ch;

@include light-theme {
border: solid 1px #c0c0d7;
background-color: #d6d6e7;
}

@include dark-theme {
border: solid 1px #5f6373;
background-color: #272a36;
}

&:focus {
border-color: var(--ifm-color-primary);
box-shadow: 0 0 0 1px var(--ifm-color-primary);
}
}

// Mirrors Docusaurus's built-in CodeBlock copy button so it looks identical.
.buttonGroup {
display: flex;
column-gap: 0.2rem;
position: absolute;
right: calc(var(--ifm-pre-padding, 1rem) / 2);
top: calc(var(--ifm-pre-padding, 1rem) / 2);

button {
display: flex;
align-items: center;
background: var(--prism-background-color, var(--ifm-code-background));
color: var(--prism-color, var(--ifm-color-content));
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: var(--ifm-global-radius);
padding: 0.4rem;
line-height: 0;
transition: opacity var(--ifm-transition-fast) ease-in-out;
opacity: 0;
}

button:focus-visible,
button:hover {
opacity: 1 !important;
}
}

.snippet:hover .buttonGroup button {
opacity: 0.4;
}

.copyButtonIcons {
position: relative;
width: 1.125rem;
height: 1.125rem;
}

.copyButtonIcon,
.copyButtonSuccessIcon {
position: absolute;
top: 0;
left: 0;
fill: currentColor;
opacity: inherit;
width: inherit;
height: inherit;
transition: all var(--ifm-transition-fast) ease;
}

.copyButtonSuccessIcon {
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.33);
opacity: 0;
color: #00d600;
}

.copyButtonCopied {
opacity: 1 !important;

.copyButtonIcon {
transform: scale(0.33);
opacity: 0;
}

.copyButtonSuccessIcon {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
transition-delay: 0.075s;
}
}
140 changes: 140 additions & 0 deletions src/components/CommandSnippet/CommandSnippet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React, { useMemo, useRef, useState } from 'react'
import clsx from 'clsx'
import Button from '@theme/CodeBlock/Buttons/Button'
import IconCopy from '@theme/Icon/Copy'
import IconSuccess from '@theme/Icon/Success'

import s from './CommandSnippet.module.scss'


type CommandSnippetProps = {
// Command template. Wrap editable parts in double braces, e.g.
// "docker run -v {{./path_to_password}}:/password -it ssvlabs/ssv-node"
template: string
}

type Segment =
| { type: 'text'; value: string }
| { type: 'field'; index: number }

// Splits the template into static text and {{editable}} segments.
const parseTemplate = (template: string): { segments: Segment[]; defaults: string[] } => {
const segments: Segment[] = []
const defaults: string[] = []
const regex = /\{\{(.*?)\}\}/g

let lastIndex = 0
let match: RegExpExecArray | null

while ((match = regex.exec(template)) !== null) {
if (match.index > lastIndex) {
segments.push({
type: 'text',
value: template.slice(lastIndex, match.index),
})
}

segments.push({
type: 'field',
index: defaults.length,
})

defaults.push(match[1])
lastIndex = regex.lastIndex
}

if (lastIndex < template.length) {
segments.push({
type: 'text',
value: template.slice(lastIndex),
})
}

return { segments, defaults }
}
Comment thread
ulieth marked this conversation as resolved.

const CommandSnippet: React.FC<CommandSnippetProps> = ({ template }) => {
const { segments, defaults } = useMemo(() => parseTemplate(template), [template])
const [values, setValues] = useState<string[]>(defaults)
const [copied, setCopied] = useState(false)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const handleChange = (index: number, next: string) => {
setValues((prev) => {
const updated = [...prev]
updated[index] = next
return updated
})
}

const assembled = useMemo(() => (
segments
.map((seg) => (seg.type === 'text' ? seg.value : values[seg.index]))
.join('')
), [segments, values])

const handleCopy = () => {
if (!navigator.clipboard) {
return
}

navigator.clipboard.writeText(assembled).then(() => {
setCopied(true)

if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}

timeoutRef.current = setTimeout(() => setCopied(false), 1000)
})
}
Comment thread
ulieth marked this conversation as resolved.

return (
<div className={s.snippet}>
<pre className={s.pre}>
<code className={s.code}>
{segments.map((seg, i) => {
if (seg.type === 'text') {
return (
<span key={i}>
{seg.value}
</span>
)
}

const value = values[seg.index] || ''

return (
<input
key={i}
className={s.field}
autoCorrect="off"
spellCheck={false}
autoCapitalize="off"
value={value}
size={Math.max(value.length, 1)}
aria-label="Editable command value"
onChange={(e) => handleChange(seg.index, e.target.value)}
/>
)
})}
Comment thread
ulieth marked this conversation as resolved.
</code>
</pre>
<div className={s.buttonGroup}>
<Button
aria-label={copied ? 'Copied' : 'Copy command to clipboard'}
title="Copy"
className={clsx(s.copyButton, copied && s.copyButtonCopied)}
onClick={handleCopy}
>
<span className={s.copyButtonIcons} aria-hidden="true">
<IconCopy className={s.copyButtonIcon} />
<IconSuccess className={s.copyButtonSuccessIcon} />
</span>
</Button>
</div>
</div>
)
}

export default CommandSnippet
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as Question } from './Question/Question'
export { default as Tooltip } from './Tooltip/Tooltip'
export { default as Footer } from './Footer/Footer'
export { default as Post } from './Post/Post'
export { default as CommandSnippet } from './CommandSnippet/CommandSnippet'