feat(rvote): Reddit-style vote column + priority trend chart
- Reddit-style vote column: prominent up/down chevrons flanking the score on each ranking proposal card, with x² cost indicator - Quadratic weight picker: compact inline buttons for +2/+3/+5 and -2 below the proposal description (supplements chevron ±1) - Priority Trends chart: SVG line chart showing how proposal scores evolve over time, with color-coded lines per proposal, end dots, grid lines, time labels, and a toggleable legend - Score history tracking: records snapshots on each vote, seeds 7 days of simulated history for demo mode - Orange for upvotes, blue for downvotes (matching rvote.online palette) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
35dd1c3d77
commit
62a96c164a
|
|
@ -34,6 +34,12 @@ interface Proposal {
|
||||||
voting_ends_at: string | null;
|
voting_ends_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Score history entry for trend chart */
|
||||||
|
interface ScoreSnapshot {
|
||||||
|
time: number; // timestamp
|
||||||
|
scores: Record<string, number>; // proposalId → score
|
||||||
|
}
|
||||||
|
|
||||||
class FolkVoteDashboard extends HTMLElement {
|
class FolkVoteDashboard extends HTMLElement {
|
||||||
private shadow: ShadowRoot;
|
private shadow: ShadowRoot;
|
||||||
private space = "";
|
private space = "";
|
||||||
|
|
@ -45,6 +51,8 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
private loading = false;
|
private loading = false;
|
||||||
private error = "";
|
private error = "";
|
||||||
private showCreateForm = false;
|
private showCreateForm = false;
|
||||||
|
private showTrendChart = true;
|
||||||
|
private scoreHistory: ScoreSnapshot[] = [];
|
||||||
private _offlineUnsubs: (() => void)[] = [];
|
private _offlineUnsubs: (() => void)[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -171,9 +179,42 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
voting_ends_at: new Date(now - 4 * day).toISOString(),
|
voting_ends_at: new Date(now - 4 * day).toISOString(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
this.recordSnapshot();
|
||||||
|
// Seed some historical snapshots for the demo trend chart
|
||||||
|
this.seedDemoHistory();
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Seed fake historical snapshots so the trend chart isn't empty */
|
||||||
|
private seedDemoHistory() {
|
||||||
|
const now = Date.now();
|
||||||
|
const day = 86400000;
|
||||||
|
const rankings = this.proposals.filter(p => p.status === "RANKING");
|
||||||
|
// Simulate 7 days of score evolution
|
||||||
|
const history: ScoreSnapshot[] = [];
|
||||||
|
for (let d = 6; d >= 0; d--) {
|
||||||
|
const snap: ScoreSnapshot = { time: now - d * day, scores: {} };
|
||||||
|
for (const p of rankings) {
|
||||||
|
// Work backwards: current score minus some random drift
|
||||||
|
const drift = Math.round((Math.random() - 0.3) * 8 * (d + 1));
|
||||||
|
snap.scores[p.id] = Math.max(0, p.score - drift);
|
||||||
|
}
|
||||||
|
history.push(snap);
|
||||||
|
}
|
||||||
|
this.scoreHistory = history;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Record current scores as a snapshot */
|
||||||
|
private recordSnapshot() {
|
||||||
|
const snap: ScoreSnapshot = { time: Date.now(), scores: {} };
|
||||||
|
for (const p of this.proposals) {
|
||||||
|
if (p.status === "RANKING") snap.scores[p.id] = p.score;
|
||||||
|
}
|
||||||
|
this.scoreHistory.push(snap);
|
||||||
|
// Keep last 50 snapshots
|
||||||
|
if (this.scoreHistory.length > 50) this.scoreHistory.shift();
|
||||||
|
}
|
||||||
|
|
||||||
private getApiBase(): string {
|
private getApiBase(): string {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
const match = path.match(/^(\/[^/]+)?\/rvote/);
|
const match = path.match(/^(\/[^/]+)?\/rvote/);
|
||||||
|
|
@ -238,6 +279,7 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
}
|
}
|
||||||
if (this.selectedProposal?.id === proposalId) this.selectedProposal = p;
|
if (this.selectedProposal?.id === proposalId) this.selectedProposal = p;
|
||||||
}
|
}
|
||||||
|
this.recordSnapshot();
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -363,16 +405,48 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
.space-stat-value { font-size: 15px; font-weight: 600; color: #818cf8; }
|
.space-stat-value { font-size: 15px; font-weight: 600; color: #818cf8; }
|
||||||
.space-stat-label { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: 0.04em; }
|
.space-stat-label { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
|
|
||||||
/* Proposal cards */
|
/* Proposal cards — Reddit layout */
|
||||||
.proposal {
|
.proposal {
|
||||||
background: #0f172a; border: 1px solid #1e293b; border-radius: 12px;
|
background: #0f172a; border: 1px solid #1e293b; border-radius: 12px;
|
||||||
padding: 16px 20px; margin-bottom: 10px; transition: border-color 0.2s;
|
margin-bottom: 10px; transition: border-color 0.2s;
|
||||||
|
display: flex; overflow: hidden;
|
||||||
}
|
}
|
||||||
.proposal:hover { border-color: #334155; }
|
.proposal:hover { border-color: #334155; }
|
||||||
.proposal-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 6px; }
|
|
||||||
|
/* Reddit-style vote column */
|
||||||
|
.vote-col {
|
||||||
|
display: flex; flex-direction: column; align-items: center;
|
||||||
|
padding: 12px 6px; gap: 2px; flex-shrink: 0; width: 56px;
|
||||||
|
background: rgba(30,41,59,0.5); border-right: 1px solid #1e293b;
|
||||||
|
}
|
||||||
|
.vote-chevron {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
width: 32px; height: 28px; border: none; border-radius: 6px;
|
||||||
|
background: transparent; cursor: pointer; color: #475569;
|
||||||
|
transition: all 0.15s; padding: 0;
|
||||||
|
}
|
||||||
|
.vote-chevron:hover { background: rgba(129,140,248,0.1); }
|
||||||
|
.vote-chevron.up:hover { color: #f97316; }
|
||||||
|
.vote-chevron.down:hover { color: #3b82f6; }
|
||||||
|
.vote-chevron:active { transform: scale(0.9); }
|
||||||
|
.vote-score {
|
||||||
|
font-size: 16px; font-weight: 800; color: #e2e8f0;
|
||||||
|
font-variant-numeric: tabular-nums; line-height: 1;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
.vote-score.positive { color: #f97316; }
|
||||||
|
.vote-score.negative { color: #3b82f6; }
|
||||||
|
.qv-cost {
|
||||||
|
font-size: 9px; color: #475569; text-align: center;
|
||||||
|
white-space: nowrap; margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right content area */
|
||||||
|
.proposal-body { flex: 1; padding: 14px 16px; min-width: 0; }
|
||||||
|
.proposal-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 4px; }
|
||||||
.proposal-title { font-size: 15px; font-weight: 600; color: #e2e8f0; cursor: pointer; line-height: 1.4; }
|
.proposal-title { font-size: 15px; font-weight: 600; color: #e2e8f0; cursor: pointer; line-height: 1.4; }
|
||||||
.proposal-title:hover { color: #818cf8; }
|
.proposal-title:hover { color: #818cf8; }
|
||||||
.proposal-desc { font-size: 13px; color: #64748b; margin-bottom: 10px; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
.proposal-desc { font-size: 13px; color: #64748b; margin-bottom: 8px; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
|
||||||
/* Status badge */
|
/* Status badge */
|
||||||
.badge {
|
.badge {
|
||||||
|
|
@ -471,6 +545,45 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
font-size: 13px; color: #94a3b8;
|
font-size: 13px; color: #94a3b8;
|
||||||
}
|
}
|
||||||
.demo-banner strong { color: #818cf8; }
|
.demo-banner strong { color: #818cf8; }
|
||||||
|
|
||||||
|
/* Quadratic weight picker (compact inline) */
|
||||||
|
.qv-weights { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
|
||||||
|
.qv-weight {
|
||||||
|
padding: 3px 8px; border-radius: 6px; border: 1px solid #334155;
|
||||||
|
background: #1e293b; color: #94a3b8; cursor: pointer; font-size: 11px;
|
||||||
|
font-weight: 600; transition: all 0.12s; white-space: nowrap;
|
||||||
|
}
|
||||||
|
.qv-weight:hover { border-color: #f97316; color: #fb923c; background: rgba(249,115,22,0.08); }
|
||||||
|
.qv-weight:active { transform: scale(0.95); }
|
||||||
|
.qv-weight.down { border-color: rgba(59,130,246,0.3); }
|
||||||
|
.qv-weight.down:hover { border-color: #3b82f6; color: #60a5fa; background: rgba(59,130,246,0.08); }
|
||||||
|
|
||||||
|
/* Trend chart */
|
||||||
|
.trend-section {
|
||||||
|
background: #0f172a; border: 1px solid #1e293b; border-radius: 12px;
|
||||||
|
padding: 16px 20px; margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.trend-header {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.trend-title { font-size: 13px; font-weight: 600; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
|
.trend-toggle {
|
||||||
|
font-size: 11px; color: #64748b; cursor: pointer; background: none;
|
||||||
|
border: 1px solid #334155; border-radius: 6px; padding: 3px 8px;
|
||||||
|
}
|
||||||
|
.trend-toggle:hover { border-color: #818cf8; color: #818cf8; }
|
||||||
|
.trend-chart { position: relative; }
|
||||||
|
.trend-chart svg { display: block; width: 100%; }
|
||||||
|
.trend-legend {
|
||||||
|
display: flex; gap: 14px; flex-wrap: wrap; margin-top: 10px;
|
||||||
|
}
|
||||||
|
.trend-legend-item {
|
||||||
|
display: flex; align-items: center; gap: 5px; font-size: 11px; color: #94a3b8;
|
||||||
|
}
|
||||||
|
.trend-legend-dot {
|
||||||
|
width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
|
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
|
||||||
|
|
@ -559,6 +672,8 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
|
|
||||||
${this.showCreateForm ? this.renderCreateForm() : ""}
|
${this.showCreateForm ? this.renderCreateForm() : ""}
|
||||||
|
|
||||||
|
${this.renderTrendChart()}
|
||||||
|
|
||||||
${voting.length > 0 ? `
|
${voting.length > 0 ? `
|
||||||
<div style="font-size:12px;font-weight:600;color:#f59e0b;text-transform:uppercase;letter-spacing:0.05em;margin:16px 0 8px;display:flex;align-items:center;gap:6px">
|
<div style="font-size:12px;font-weight:600;color:#f59e0b;text-transform:uppercase;letter-spacing:0.05em;margin:16px 0 8px;display:flex;align-items:center;gap:6px">
|
||||||
<span>🗳</span> Final Voting (${voting.length})
|
<span>🗳</span> Final Voting (${voting.length})
|
||||||
|
|
@ -588,9 +703,23 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
const statusColor = this.getStatusColor(p.status);
|
const statusColor = this.getStatusColor(p.status);
|
||||||
const pct = Math.min(100, (p.score / threshold) * 100);
|
const pct = Math.min(100, (p.score / threshold) * 100);
|
||||||
const totalFinal = p.final_yes + p.final_no + p.final_abstain;
|
const totalFinal = p.final_yes + p.final_no + p.final_abstain;
|
||||||
|
const scoreClass = p.score > 0 ? 'positive' : p.score < 0 ? 'negative' : '';
|
||||||
|
|
||||||
|
// SVG chevrons for Reddit-style voting
|
||||||
|
const upChevron = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>`;
|
||||||
|
const downChevron = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="proposal" data-pid="${p.id}">
|
<div class="proposal" data-pid="${p.id}">
|
||||||
|
${p.status === "RANKING" ? `
|
||||||
|
<div class="vote-col">
|
||||||
|
<button class="vote-chevron up" data-vote-weight="1" data-vote-id="${p.id}" title="Upvote (+1 credit)">${upChevron}</button>
|
||||||
|
<span class="vote-score ${scoreClass}">${Math.round(p.score)}</span>
|
||||||
|
<button class="vote-chevron down" data-vote-weight="-1" data-vote-id="${p.id}" title="Downvote (-1 credit)">${downChevron}</button>
|
||||||
|
<div class="qv-cost">x² cost</div>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
<div class="proposal-body">
|
||||||
<div class="proposal-top">
|
<div class="proposal-top">
|
||||||
<span class="proposal-title" data-proposal="${p.id}">${this.esc(p.title)}</span>
|
<span class="proposal-title" data-proposal="${p.id}">${this.esc(p.title)}</span>
|
||||||
<span class="badge" style="background:${statusColor}18;color:${statusColor}">${this.getStatusIcon(p.status)} ${p.status}</span>
|
<span class="badge" style="background:${statusColor}18;color:${statusColor}">${this.getStatusIcon(p.status)} ${p.status}</span>
|
||||||
|
|
@ -599,13 +728,12 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
|
|
||||||
${p.status === "RANKING" ? `
|
${p.status === "RANKING" ? `
|
||||||
<div class="score-row">
|
<div class="score-row">
|
||||||
<span class="score-label">Score: <span class="score-value" style="color:#3b82f6">${Math.round(p.score)}</span> / ${threshold}</span>
|
<span class="score-label"><span class="score-value" style="color:#3b82f6">${Math.round(p.score)}</span> / ${threshold}</span>
|
||||||
<div class="score-bar"><div class="score-fill" style="width:${pct}%;background:linear-gradient(90deg,#3b82f6,#818cf8)"></div></div>
|
<div class="score-bar"><div class="score-fill" style="width:${pct}%;background:linear-gradient(90deg,#f97316,#818cf8)"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="vote-row">
|
<div class="qv-weights">
|
||||||
${[1, 2, 3, 5].map(w => `<button class="vote-btn" data-vote-weight="${w}" data-vote-id="${p.id}">+${w} <span style="font-size:11px;color:#64748b">(${w * w}cr)</span></button>`).join("")}
|
${[2, 3, 5].map(w => `<button class="qv-weight" data-vote-weight="${w}" data-vote-id="${p.id}">+${w} (${w*w}cr)</button>`).join("")}
|
||||||
<span class="vote-sep">|</span>
|
<button class="qv-weight down" data-vote-weight="-2" data-vote-id="${p.id}">-2</button>
|
||||||
${[-1, -2].map(w => `<button class="vote-btn" data-vote-weight="${w}" data-vote-id="${p.id}" style="border-color:rgba(239,68,68,0.3);color:#ef4444">${w}</button>`).join("")}
|
|
||||||
</div>
|
</div>
|
||||||
` : ""}
|
` : ""}
|
||||||
|
|
||||||
|
|
@ -647,6 +775,92 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
${p.status === "RANKING" ? `<span style="color:#3b82f6">${Math.round(threshold - p.score)} to advance</span>` : ""}
|
${p.status === "RANKING" ? `<span style="color:#3b82f6">${Math.round(threshold - p.score)} to advance</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Render an SVG line chart showing score trends over time */
|
||||||
|
private renderTrendChart(): string {
|
||||||
|
const ranking = this.proposals.filter(p => p.status === "RANKING");
|
||||||
|
if (ranking.length === 0 || this.scoreHistory.length < 2) return "";
|
||||||
|
|
||||||
|
const COLORS = ["#f97316", "#3b82f6", "#a855f7", "#22c55e", "#f43f5e", "#06b6d4", "#eab308"];
|
||||||
|
const W = 500, H = 160, PAD_L = 36, PAD_R = 12, PAD_T = 8, PAD_B = 24;
|
||||||
|
const plotW = W - PAD_L - PAD_R;
|
||||||
|
const plotH = H - PAD_T - PAD_B;
|
||||||
|
|
||||||
|
// Get all scores across history for scale
|
||||||
|
let maxScore = 10;
|
||||||
|
for (const snap of this.scoreHistory) {
|
||||||
|
for (const s of Object.values(snap.scores)) {
|
||||||
|
if (s > maxScore) maxScore = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
maxScore = Math.ceil(maxScore / 10) * 10; // round up to nearest 10
|
||||||
|
|
||||||
|
const xScale = (i: number) => PAD_L + (i / (this.scoreHistory.length - 1)) * plotW;
|
||||||
|
const yScale = (v: number) => PAD_T + plotH - (v / maxScore) * plotH;
|
||||||
|
|
||||||
|
// Grid lines
|
||||||
|
const gridLines: string[] = [];
|
||||||
|
const gridSteps = 4;
|
||||||
|
for (let i = 0; i <= gridSteps; i++) {
|
||||||
|
const val = Math.round((maxScore / gridSteps) * i);
|
||||||
|
const y = yScale(val);
|
||||||
|
gridLines.push(`<line x1="${PAD_L}" y1="${y}" x2="${W - PAD_R}" y2="${y}" stroke="#1e293b" stroke-width="1"/>`);
|
||||||
|
gridLines.push(`<text x="${PAD_L - 6}" y="${y + 4}" text-anchor="end" fill="#475569" font-size="10">${val}</text>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time labels
|
||||||
|
const timeLabels: string[] = [];
|
||||||
|
const firstTime = this.scoreHistory[0].time;
|
||||||
|
const lastTime = this.scoreHistory[this.scoreHistory.length - 1].time;
|
||||||
|
const dayDiff = Math.max(1, Math.round((lastTime - firstTime) / 86400000));
|
||||||
|
const labelStep = Math.max(1, Math.floor(this.scoreHistory.length / 5));
|
||||||
|
for (let i = 0; i < this.scoreHistory.length; i += labelStep) {
|
||||||
|
const d = new Date(this.scoreHistory[i].time);
|
||||||
|
const label = dayDiff > 1 ? `${d.getMonth() + 1}/${d.getDate()}` : `${d.getHours()}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||||
|
timeLabels.push(`<text x="${xScale(i)}" y="${H - 4}" text-anchor="middle" fill="#475569" font-size="10">${label}</text>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lines for each proposal
|
||||||
|
const lines: string[] = [];
|
||||||
|
const legend: string[] = [];
|
||||||
|
ranking.forEach((p, pIdx) => {
|
||||||
|
const color = COLORS[pIdx % COLORS.length];
|
||||||
|
const points: string[] = [];
|
||||||
|
for (let i = 0; i < this.scoreHistory.length; i++) {
|
||||||
|
const score = this.scoreHistory[i].scores[p.id] ?? 0;
|
||||||
|
points.push(`${xScale(i)},${yScale(score)}`);
|
||||||
|
}
|
||||||
|
lines.push(`<polyline points="${points.join(" ")}" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"/>`);
|
||||||
|
// End dot
|
||||||
|
const lastScore = this.scoreHistory[this.scoreHistory.length - 1].scores[p.id] ?? 0;
|
||||||
|
const lastX = xScale(this.scoreHistory.length - 1);
|
||||||
|
const lastY = yScale(lastScore);
|
||||||
|
lines.push(`<circle cx="${lastX}" cy="${lastY}" r="4" fill="${color}" stroke="#0f172a" stroke-width="2"/>`);
|
||||||
|
// Legend
|
||||||
|
const shortTitle = p.title.length > 30 ? p.title.slice(0, 28) + '...' : p.title;
|
||||||
|
legend.push(`<span class="trend-legend-item"><span class="trend-legend-dot" style="background:${color}"></span>${this.esc(shortTitle)}</span>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="trend-section">
|
||||||
|
<div class="trend-header">
|
||||||
|
<span class="trend-title">Priority Trends</span>
|
||||||
|
<button class="trend-toggle" data-toggle-trend>${this.showTrendChart ? 'Hide' : 'Show'}</button>
|
||||||
|
</div>
|
||||||
|
${this.showTrendChart ? `
|
||||||
|
<div class="trend-chart">
|
||||||
|
<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet">
|
||||||
|
${gridLines.join("")}
|
||||||
|
${timeLabels.join("")}
|
||||||
|
${lines.join("")}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="trend-legend">${legend.join("")}</div>
|
||||||
|
` : ""}
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -808,6 +1022,12 @@ class FolkVoteDashboard extends HTMLElement {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toggle trend chart
|
||||||
|
this.shadow.querySelector("[data-toggle-trend]")?.addEventListener("click", () => {
|
||||||
|
this.showTrendChart = !this.showTrendChart;
|
||||||
|
this.render();
|
||||||
|
});
|
||||||
|
|
||||||
// Toggle create form
|
// Toggle create form
|
||||||
this.shadow.querySelector("[data-toggle-create]")?.addEventListener("click", () => {
|
this.shadow.querySelector("[data-toggle-create]")?.addEventListener("click", () => {
|
||||||
this.showCreateForm = !this.showCreateForm;
|
this.showCreateForm = !this.showCreateForm;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue