rspace-online/lib/folk-token-mint.ts

403 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
const styles = css`
:host {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-width: 280px;
min-height: 200px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
color: white;
border-radius: 8px 8px 0 0;
font-size: 12px;
font-weight: 600;
cursor: move;
}
.header-title {
display: flex;
align-items: center;
gap: 6px;
}
.header-title .icon {
font-size: 16px;
}
.header-title .symbol {
opacity: 0.8;
font-weight: 400;
font-size: 11px;
}
.header-actions button {
background: transparent;
border: none;
color: white;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
font-size: 14px;
}
.header-actions button:hover {
background: rgba(255, 255, 255, 0.2);
}
.mint-body {
padding: 12px;
}
.desc {
font-size: 11px;
color: #64748b;
margin-bottom: 10px;
line-height: 1.4;
}
.supply-row {
display: flex;
justify-content: space-between;
font-size: 12px;
margin-bottom: 4px;
}
.supply-row .label {
color: #64748b;
}
.supply-row .value {
font-weight: 600;
color: #1e293b;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
margin: 8px 0 12px;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.edit-form {
padding: 8px 12px;
border-top: 1px solid #e2e8f0;
display: flex;
flex-direction: column;
gap: 4px;
}
.edit-row {
display: flex;
gap: 4px;
}
.edit-form input {
flex: 1;
padding: 5px 8px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 11px;
outline: none;
}
.edit-form input:focus {
border-color: #8b5cf6;
}
.edit-form input.short {
width: 70px;
flex: 0;
}
.edit-form input.color-input {
width: 36px;
flex: 0;
padding: 2px;
cursor: pointer;
}
`;
declare global {
interface HTMLElementTagNameMap {
"folk-token-mint": FolkTokenMint;
}
}
export class FolkTokenMint extends FolkShape {
static override tagName = "folk-token-mint";
static {
const sheet = new CSSStyleSheet();
const parentRules = Array.from(FolkShape.styles.cssRules)
.map((r) => r.cssText)
.join("\n");
const childRules = Array.from(styles.cssRules)
.map((r) => r.cssText)
.join("\n");
sheet.replaceSync(`${parentRules}\n${childRules}`);
this.styles = sheet;
}
#tokenName = "New Token";
#tokenSymbol = "TKN";
#description = "";
#totalSupply = 1000;
#issuedSupply = 0;
#tokenColor = "#8b5cf6";
#tokenIcon = "🪙";
#createdBy = "";
#createdAt = new Date().toISOString();
#headerEl: HTMLElement | null = null;
#summaryEl: HTMLElement | null = null;
#progressEl: HTMLElement | null = null;
#descEl: HTMLElement | null = null;
get tokenName() { return this.#tokenName; }
set tokenName(v: string) {
this.#tokenName = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get tokenSymbol() { return this.#tokenSymbol; }
set tokenSymbol(v: string) {
this.#tokenSymbol = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get description() { return this.#description; }
set description(v: string) {
this.#description = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get totalSupply() { return this.#totalSupply; }
set totalSupply(v: number) {
this.#totalSupply = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get issuedSupply() { return this.#issuedSupply; }
set issuedSupply(v: number) {
this.#issuedSupply = v;
this.#render();
}
get tokenColor() { return this.#tokenColor; }
set tokenColor(v: string) {
this.#tokenColor = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get tokenIcon() { return this.#tokenIcon; }
set tokenIcon(v: string) {
this.#tokenIcon = v;
this.#render();
this.dispatchEvent(new CustomEvent("content-change"));
}
get createdBy() { return this.#createdBy; }
set createdBy(v: string) {
this.#createdBy = v;
}
get createdAt() { return this.#createdAt; }
set createdAt(v: string) {
this.#createdAt = v;
}
get remaining() {
return this.#totalSupply - this.#issuedSupply;
}
override createRenderRoot() {
const root = super.createRenderRoot();
const wrapper = document.createElement("div");
wrapper.innerHTML = html`
<div class="header" style="background: ${this.#tokenColor}">
<span class="header-title">
<span class="icon">${this.#tokenIcon}</span>
<span class="name">${this.#tokenName}</span>
<span class="symbol">${this.#tokenSymbol}</span>
</span>
<div class="header-actions">
<button class="close-btn" title="Close">×</button>
</div>
</div>
<div class="mint-body">
<div class="desc"></div>
<div class="supply-summary"></div>
<div class="progress-bar"><div class="progress-fill"></div></div>
</div>
<div class="edit-form">
<div class="edit-row">
<input type="text" placeholder="Token name" class="name-input" />
<input type="text" placeholder="SYM" class="symbol-input short" maxlength="6" />
</div>
<div class="edit-row">
<input type="number" placeholder="Total supply" class="supply-input" min="1" />
<input type="color" class="color-input" title="Token color" />
</div>
<div class="edit-row">
<input type="text" placeholder="Description (optional)" class="desc-input" />
</div>
</div>
`;
const slot = root.querySelector("slot");
const containerDiv = slot?.parentElement as HTMLElement;
if (containerDiv) {
containerDiv.replaceWith(wrapper);
}
this.#headerEl = wrapper.querySelector(".header");
this.#summaryEl = wrapper.querySelector(".supply-summary");
this.#progressEl = wrapper.querySelector(".progress-fill");
this.#descEl = wrapper.querySelector(".desc");
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
const nameInput = wrapper.querySelector(".name-input") as HTMLInputElement;
const symbolInput = wrapper.querySelector(".symbol-input") as HTMLInputElement;
const supplyInput = wrapper.querySelector(".supply-input") as HTMLInputElement;
const colorInput = wrapper.querySelector(".color-input") as HTMLInputElement;
const descInput = wrapper.querySelector(".desc-input") as HTMLInputElement;
// Set initial input values
nameInput.value = this.#tokenName;
symbolInput.value = this.#tokenSymbol;
supplyInput.value = String(this.#totalSupply);
colorInput.value = this.#tokenColor;
descInput.value = this.#description;
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.dispatchEvent(new CustomEvent("close"));
});
const stopProp = (e: Event) => e.stopPropagation();
nameInput.addEventListener("click", stopProp);
symbolInput.addEventListener("click", stopProp);
supplyInput.addEventListener("click", stopProp);
colorInput.addEventListener("click", stopProp);
descInput.addEventListener("click", stopProp);
nameInput.addEventListener("change", () => {
if (nameInput.value.trim()) this.tokenName = nameInput.value.trim();
});
nameInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.stopPropagation(); nameInput.blur(); }
});
symbolInput.addEventListener("change", () => {
if (symbolInput.value.trim()) this.tokenSymbol = symbolInput.value.trim().toUpperCase();
});
symbolInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.stopPropagation(); symbolInput.blur(); }
});
supplyInput.addEventListener("change", () => {
const val = Number(supplyInput.value);
if (val > 0) this.totalSupply = val;
});
supplyInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.stopPropagation(); supplyInput.blur(); }
});
colorInput.addEventListener("input", () => {
this.tokenColor = colorInput.value;
});
descInput.addEventListener("change", () => {
this.description = descInput.value.trim();
});
descInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.stopPropagation(); descInput.blur(); }
});
this.#render();
return root;
}
#escapeHtml(text: string): string {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
#render() {
if (!this.#headerEl || !this.#summaryEl || !this.#progressEl) return;
this.#headerEl.style.background = this.#tokenColor;
const nameSpan = this.#headerEl.querySelector(".name");
const symbolSpan = this.#headerEl.querySelector(".symbol");
const iconSpan = this.#headerEl.querySelector(".icon");
if (nameSpan) nameSpan.textContent = this.#tokenName;
if (symbolSpan) symbolSpan.textContent = this.#tokenSymbol;
if (iconSpan) iconSpan.textContent = this.#tokenIcon;
if (this.#descEl) {
this.#descEl.textContent = this.#description;
this.#descEl.style.display = this.#description ? "block" : "none";
}
const pct = this.#totalSupply > 0 ? (this.#issuedSupply / this.#totalSupply) * 100 : 0;
this.#summaryEl.innerHTML = `
<div class="supply-row">
<span class="label">Total Supply</span>
<span class="value">${this.#totalSupply.toLocaleString()}</span>
</div>
<div class="supply-row">
<span class="label">Issued</span>
<span class="value">${this.#issuedSupply.toLocaleString()}</span>
</div>
<div class="supply-row">
<span class="label">Remaining</span>
<span class="value">${this.remaining.toLocaleString()}</span>
</div>
`;
this.#progressEl.style.width = `${Math.min(pct, 100)}%`;
this.#progressEl.style.background = this.#tokenColor;
}
override toJSON() {
return {
...super.toJSON(),
type: "folk-token-mint",
tokenName: this.#tokenName,
tokenSymbol: this.#tokenSymbol,
description: this.#description,
totalSupply: this.#totalSupply,
issuedSupply: this.#issuedSupply,
tokenColor: this.#tokenColor,
tokenIcon: this.#tokenIcon,
createdBy: this.#createdBy,
createdAt: this.#createdAt,
};
}
}