Resolve "WIP: UTXO Selection and Storage"
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<Tooltip :show="showTooltip && !isCopied">
|
||||
<button
|
||||
class="copy-button"
|
||||
:class="{ 'copied': isCopied }"
|
||||
@click="handleCopy"
|
||||
@mouseenter="showTooltip = true"
|
||||
@mouseleave="showTooltip = false"
|
||||
>
|
||||
<Transition mode="out-in">
|
||||
<img v-if="!isCopied" key="icon" src="../icons/copy.svg" alt="Copy" class="copy-icon" />
|
||||
<span v-else key="text" class="copied-text">Copied!</span>
|
||||
</Transition>
|
||||
</button>
|
||||
<template #content>Copy</template>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Tooltip from './Tooltip.vue';
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
content: ''
|
||||
});
|
||||
|
||||
const isCopied = ref(false);
|
||||
const showTooltip = ref(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(props.content);
|
||||
isCopied.value = true;
|
||||
showTooltip.value = false;
|
||||
setTimeout(() => {
|
||||
isCopied.value = false;
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tooltip-container {
|
||||
position: absolute !important;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: relative;
|
||||
padding: 5px 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background: var(--vp-home-hero-name-background);
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0.9;
|
||||
width: 2.2em;
|
||||
height: 2.2em;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
opacity: 1;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.copy-button.copied {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.copied-text {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="labelled-section-container">
|
||||
<div class="labelled-section">
|
||||
{{ content || ' ' }}
|
||||
</div>
|
||||
<label class="section-label" :class="{ 'is-floating': hasContent }">
|
||||
{{ label }}
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
export interface Props {
|
||||
content?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
content: '',
|
||||
label: ''
|
||||
})
|
||||
|
||||
const hasContent = computed(() => props.content.length > 0)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.labelled-section-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.labelled-section {
|
||||
box-sizing: content-box;
|
||||
padding: 10px 8px;
|
||||
font-size: 1rem;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg);
|
||||
line-height: 1.5;
|
||||
min-height: calc(1.5em);
|
||||
font-family: monospace;
|
||||
max-width: 100%;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0 4px;
|
||||
font-size: 1em;
|
||||
font-family: monospace;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.section-label.is-floating {
|
||||
top: 0;
|
||||
font-size: 0.8em;
|
||||
transform: translateY(-50%);
|
||||
font-style: normal;
|
||||
color: var(--vp-c-text-2);
|
||||
opacity: 1.0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="input-wrapper">
|
||||
<h2>Input</h2>
|
||||
|
||||
<div class="form-group-row" :class="{ 'extended': state.selectedFromFunction?.extendedInput }">
|
||||
<div class="form-group select-group">
|
||||
<label for="functionSelect">Select Function</label>
|
||||
<select v-model="state.selectedFromFunction">
|
||||
<option v-for="fromFunction of props.fromFunctions" :key="fromFunction.label" :value="fromFunction">{{ fromFunction.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group input-group" :class="{ 'extended': state.selectedFromFunction?.extendedInput }">
|
||||
<template v-for="(_input, i) of state.selectedFromFunction.inputs" :key="i">
|
||||
<label for="field">{{ state.selectedFromFunction.inputs[i] }}</label>
|
||||
<div>
|
||||
<textarea
|
||||
v-if="state.selectedFromFunction?.extendedInput"
|
||||
v-model="state.selectedFromFunction.models[i]"
|
||||
class="extended-input"
|
||||
@input="autoGrow($event.target)"
|
||||
></textarea>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
v-model="state.selectedFromFunction.models[i]"
|
||||
/>
|
||||
<div v-if="state.error" class="text-error">{{ state.error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="gradient-button" @click="() => setPrimitive(() => state.selectedFromFunction.callback(state.selectedFromFunction.models))">Create Output</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section output-section">
|
||||
<h2>Output</h2>
|
||||
|
||||
<template v-for="(toFunction, i) of toFunctions" :key="i">
|
||||
<div>
|
||||
<div class="output-box-container">
|
||||
<LabelledSection :label="toFunction.label" :content="getPrimitive(() => toFunction.callback(state.primitive))" />
|
||||
<CopyButton :content="getPrimitive(() => toFunction.callback(state.primitive))" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="state.methods.length && state.primitive">
|
||||
<h2>Methods</h2>
|
||||
|
||||
<div class="form-group-row" :class="{ 'extended': state.selectedMethod?.extendedInput }">
|
||||
<div class="form-group select-group">
|
||||
<label for="functionSelect">Select Function</label>
|
||||
<select v-model="state.selectedMethod">
|
||||
<option v-for="method of props.methods" :key="method.label" :value="method">{{ method.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group input-group" :class="{ 'extended': state.selectedMethod?.extendedInput }">
|
||||
<template v-for="(_input, i) of state.selectedMethod.inputs" :key="i">
|
||||
<label for="field">{{ state.selectedMethod.inputs[i] }}</label>
|
||||
<div>
|
||||
<textarea
|
||||
v-if="state.selectedMethod?.extendedInput"
|
||||
v-model="state.selectedMethod.models[i]"
|
||||
class="extended-input"
|
||||
@input="autoGrow($event.target)"
|
||||
></textarea>
|
||||
<input
|
||||
v-else
|
||||
type="text"
|
||||
v-model="state.selectedMethod.models[i]"
|
||||
/>
|
||||
<div v-if="state.error" class="text-error">{{ state.error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="gradient-button" @click="() => executeMethod(state.selectedMethod.callback(state.primitive, state.selectedMethod.models))">Execute Method</button>
|
||||
</div>
|
||||
<!-- Result -->
|
||||
<div>
|
||||
<div class="output-box-container">
|
||||
<LabelledSection label="Result" :content="state.selectedMethod.result" />
|
||||
<CopyButton :content="state.selectedMethod.result" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup generic="Primitive" lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import LabelledSection from './LabelledSection.vue';
|
||||
import CopyButton from './CopyButton.vue';
|
||||
|
||||
export interface FromFunction<Primitive> {
|
||||
// The label of the function
|
||||
label: string,
|
||||
|
||||
// Array of inputs that the function takes
|
||||
inputs: Array<string>,
|
||||
|
||||
// Array of models that the input stores its value in
|
||||
models: Array<any>,
|
||||
|
||||
// Function that takes the models and returns the primitive
|
||||
callback: (value: Array<any>) => Primitive,
|
||||
|
||||
// Whether the input is extended onto a new line
|
||||
extendedInput?: boolean
|
||||
}
|
||||
|
||||
export interface ToFunction<Primitive> {
|
||||
label: string;
|
||||
callback: (primitive: Primitive) => string;
|
||||
result?: string;
|
||||
}
|
||||
|
||||
export interface Method<Primitive> {
|
||||
// The label of the function
|
||||
label: string;
|
||||
|
||||
// Array of inputs that the function takes
|
||||
inputs: Array<string>,
|
||||
|
||||
// Array of models that the input stores its value in
|
||||
models: Array<any>,
|
||||
|
||||
// Result of method execution,
|
||||
result?: string;
|
||||
|
||||
// Function that takes the models and returns the primitive
|
||||
callback: (primitive: Primitive, value: Array<any>) => string;
|
||||
|
||||
// Whether the input is extended onto a new line
|
||||
extendedInput?: boolean
|
||||
}
|
||||
|
||||
export interface Props<Primitive> {
|
||||
fromFunctions: Array<FromFunction<Primitive>>,
|
||||
toFunctions: Array<ToFunction<Primitive>>,
|
||||
methods?: Array<Method<Primitive>>,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props<Primitive>>(), {
|
||||
fromFunctions: () => [],
|
||||
toFunctions: () => [],
|
||||
methods: () => [],
|
||||
});
|
||||
|
||||
const state: Ref<{
|
||||
fromFunctions: Array<FromFunction<Primitive>>,
|
||||
toFunctions: Array<ToFunction<Primitive>>,
|
||||
methods: Array<Method<Primitive>>,
|
||||
|
||||
// Selected In Function
|
||||
selectedFromFunction: FromFunction<Primitive>,
|
||||
|
||||
// Selected Method
|
||||
selectedMethod?: Method<Primitive>,
|
||||
|
||||
// The Primitive.
|
||||
primitive?: Primitive,
|
||||
|
||||
// Error to display if an invalid input was given.
|
||||
error?: string;
|
||||
}> = ref({
|
||||
// From functions, to functions and methods.
|
||||
fromFunctions: [ ...props.fromFunctions ],
|
||||
toFunctions: [ ...props.toFunctions ],
|
||||
methods: [ ...props.methods ],
|
||||
|
||||
// The selected function.
|
||||
selectedFromFunction: props.fromFunctions[0],
|
||||
|
||||
// The selected method.
|
||||
selectedMethod: props.methods[0] || undefined,
|
||||
|
||||
// The Primitive.
|
||||
primitive: undefined,
|
||||
})
|
||||
|
||||
async function setPrimitive(functionToExecute: () => Primitive | Promise<Primitive>): void {
|
||||
try {
|
||||
state.value.primitive = await functionToExecute();
|
||||
state.value.error = undefined;
|
||||
|
||||
state.value.toFunctions.forEach((wrappedMethod, i) => {
|
||||
state.value.toFunctions[i].result = wrappedMethod.callback(state.value.primitive)
|
||||
});
|
||||
} catch(error: any) {
|
||||
state.value.error = `${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
function getPrimitive(functionToExecute: () => string): string {
|
||||
try {
|
||||
if(!state.value.primitive) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return functionToExecute();
|
||||
} catch(error: any) {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeMethod(wrappedMethod: () => string | Promise<string>): string {
|
||||
try {
|
||||
if(!state.value.primitive) {
|
||||
return '';
|
||||
}
|
||||
|
||||
state.value.selectedMethod.result = await wrappedMethod;
|
||||
} catch(error: any) {
|
||||
return error.message;
|
||||
}
|
||||
};
|
||||
|
||||
// Select the first "from" function.
|
||||
state.value.selectedFromFunction = state.value.fromFunctions[0];
|
||||
|
||||
const autoGrow = (element: HTMLTextAreaElement) => {
|
||||
element.style.height = 'auto'
|
||||
element.style.height = `${Math.min(element.scrollHeight, 300)}px`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.text-error {
|
||||
color: #f46666;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="tooltip-container">
|
||||
<slot></slot>
|
||||
<Transition>
|
||||
<div v-if="show" class="tooltip" :class="position">
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
show: boolean;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
position: 'top',
|
||||
show: false
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
background: var(--vp-c-text-1);
|
||||
color: var(--vp-c-bg);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tooltip.top {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-6px);
|
||||
}
|
||||
|
||||
.tooltip.bottom {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(6px);
|
||||
}
|
||||
|
||||
.tooltip.left {
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(-6px);
|
||||
}
|
||||
|
||||
.tooltip.right {
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) translateX(6px);
|
||||
}
|
||||
|
||||
.v-enter-active,
|
||||
.v-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user