Resolve "WIP: UTXO Selection and Storage"

This commit is contained in:
Jim Hamill
2025-01-11 08:39:24 +00:00
parent f934216d68
commit 33fd9bac4d
156 changed files with 13304 additions and 4595 deletions
@@ -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>