[[ legendary && "✦ LEGENDARY" ]]
[[ name ]]
[[ class ?? "Adventurer" ]][[ legendary ? origin : element ?? "unknown" ]]
HP [[ hp ]]
ATK [[ attack ]]
DEF [[ defense ?? 50 ]]
- [[ skill ]]
[[ lore ?? "" ]]
Use template scripts, TemplateHost, and onDisconnect for instance behavior.
[[ legendary ? origin : element ?? "unknown" ]]
[[ lore ?? "" ]]
The following example assumes that you have imported the component and set up the theme.
<sv-template id="card-template"> <template> <!-- Template script: runs on every instance --> <script type="module"> const { refs, props, onDisconnect } = TemplateHost;
const detailsBtnHandler = () => { alert( `📜 ${props.name}` + `\nClass: ${props.class ?? "Adventurer"}` + `\nElement: ${props.element ?? "neutral"}` + `\nHP: ${props.hp} | ATK: ${props.attack} | DEF: ${props.defense ?? 50}` + `\nSkills: ${props.skills.join(", ")}` + (props.lore ? `\n\n"${props.lore}"` : "") ); };
refs.detailsBtn.addEventListener("click", detailsBtnHandler);
onDisconnect(() => { refs.detailsBtn.removeEventListener("click", detailsBtnHandler); }); </script>
<!-- Template markup --> <article class="card" data-element="[[ element ?? 'neutral' ]]"> <!-- ternary: remove legendary badge if not legendary --> <div class="legendary-badge" template-remove="[[ legendary ? false : true ]]">[[ legendary && "✦ LEGENDARY" ]]</div>
<div class="card-inner"> <header class="card-header"> <h2 class="card-name">[[ name ]]</h2> <span class="card-class">[[ class ?? "Adventurer" ]]</span> </header>
<div class="card-avatar"> <div class="avatar-ring"></div> <div class="avatar-icon" title="[[ name ]]">[[ icon ]]</div> </div>
<!-- ternary: legendary chars show their named origin, others show their element --> <p class="card-origin">[[ legendary ? origin : element ?? "unknown" ]]</p>
<div class="card-stats"> <div class="stat"> <span class="stat-label">HP</span> <span class="stat-bar-wrap"> <span class="stat-bar" style="--val: [[hp]]%"></span> </span> <span class="stat-value">[[ hp ]]</span> </div>
<div class="stat"> <span class="stat-label">ATK</span> <span class="stat-bar-wrap"> <span class="stat-bar atk" style="--val: [[attack]]%"></span> </span> <span class="stat-value">[[ attack ]]</span> </div>
<div class="stat"> <span class="stat-label">DEF</span> <span class="stat-bar-wrap"> <span class="stat-bar def" style="--val: [[defense ?? 50]]%"></span> </span> <span class="stat-value">[[ defense ?? 50 ]]</span> </div> </div>
<!-- repeat: one <li> cloned per entry in the skills array --> <ul class="card-skills"> <li class="skill-tag" template-for=",skill of skills">[[ skill ]]</li> </ul>
<p class="card-lore">[[ lore ?? "" ]]</p>
<!-- ternary: button label differs for legendary characters --> <button type="button" class="card-btn" template-ref="detailsBtn">[[ legendary ? "View Legend" : "View Details" ]]</button> </div> </article> </template>
<!-- Scoped style -->248 collapsed lines
<style> .card { position: relative; padding: 1em; overflow: visible; background: var(--sv-gray-6); border: 1px solid var(--sv-gray-5); border-radius: 16px; transition: transform 0.3s ease, box-shadow 0.3s ease;
&::before { position: absolute; inset-block-start: -1px; inset-inline: 16px; block-size: 4px; content: ""; background: linear-gradient(90deg, #0000, var(--sv-accent), #0000); }
&:hover { box-shadow: 0 0 0 1px var(--sv-accent), 0 0 32px var(--sv-accent-glow); transform: translateY(-6px) scale(1.001); }
&[data-element="fire"] { --sv-accent: #ff6b35; --sv-accent-glow: rgb(255 107 53 / 25%); }
&[data-element="earth"] { --sv-accent: #7dab57; --sv-accent-glow: rgb(125 171 87 / 25%); }
&[data-element="void"] { --sv-accent: #b56eff; --sv-accent-glow: rgb(181 110 255 / 25%); }
&[data-element="lightning"] { --sv-accent: #9f9036; --sv-accent-glow: rgb(255 224 51 / 25%); }
&[data-element="nature"] { --sv-accent: #52d68a; --sv-accent-glow: rgb(82 214 138 / 25%); }
&[data-element="ice"] { --sv-accent: #4eacc4; --sv-accent-glow: rgb(99 216 245 / 25%); } }
.legendary-badge { position: absolute; inset-block-start: -12px; inset-inline-start: 50%; z-index: 2; padding: 3px 14px; font-family: Cinzel, serif; font-size: 0.62rem; font-weight: 700; color: #1a1000; letter-spacing: 0.15em; white-space: nowrap; background: linear-gradient(90deg, #7a5a1a, var(--gold), #7a5a1a); border-radius: 20px; box-shadow: 0 0 14px var(--gold-glow); transform: translateX(-50%); }
.card-inner { display: flex; flex-direction: column; gap: 1em; justify-content: space-between; block-size: 100%; }
.card-header { display: flex; flex-direction: column; gap: 0.2rem; }
.card-name { font-family: Cinzel, serif; font-size: 1.3rem; font-weight: 700; letter-spacing: 0.06em; }
.card-class { font-size: 0.82rem; color: var(--sv-accent); text-transform: uppercase; letter-spacing: 0.12em; opacity: 0.85; }
.card-avatar { position: relative; align-self: center; inline-size: 72px; block-size: 72px; }
.avatar-ring { position: absolute; inset: 0; border: 2px solid var(--sv-accent); border-radius: 50%; opacity: 0.6; animation: pulse 3s ease-in-out infinite; }
.avatar-icon { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 2.2rem; background: var(--sv-gray-7); border-radius: 50%; }
.card-origin { font-size: 0.78rem; font-style: italic; color: var(--gold); text-align: center; letter-spacing: 0.06em; opacity: 0.8; }
.card-stats { display: flex; flex-direction: column; gap: 0.5rem; }
.stat { display: grid; grid-template-columns: 34px 1fr 36px; gap: 0.5rem; align-items: center; }
.stat-label { font-family: Cinzel, serif; font-size: 0.65rem; color: var(--text-dim); letter-spacing: 0.12em; }
.stat-bar-wrap { block-size: 5px; overflow: hidden; background: var(--sv-gray-5); border-radius: 999px; }
.stat-bar { display: block; inline-size: var(--val, 0%); block-size: 100%; background: var(--sv-accent); border-radius: 999px; transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1); }
.stat-bar.atk { background: linear-gradient(90deg, var(--sv-accent), #ff9a6c); }
.stat-bar.def { background: linear-gradient(90deg, var(--sv-accent), #7ed4ff); }
.stat-value { font-size: 0.8rem; font-variant-numeric: tabular-nums; color: var(--text-dim); text-align: end; }
.card-skills { display: flex; flex-wrap: wrap; gap: 0.35rem; padding: 0; list-style: none; }
.skill-tag { padding: 0.2rem 0.6rem; font-size: 0.7rem; color: var(--sv-accent); letter-spacing: 0.08em; background: color-mix(in srgb, var(--sv-accent) 10%, transparent); border: 1px solid color-mix(in srgb, var(--sv-accent) 35%, transparent); border-radius: 999px; }
.card-lore { min-block-size: 1.4rem; padding-inline-start: 0.65rem; font-size: 0.9rem; font-style: italic; line-height: 1.55; color: var(--text-dim); border-inline-start: 2px solid var(--sv-accent); opacity: 0.85;
&:empty { border-inline-start: none; } }
.card-btn { align-self: flex-start; padding: 0.4rem 1rem; font-family: Cinzel, serif; font-size: 0.72rem; color: var(--sv-accent); letter-spacing: 0.12em; cursor: pointer; background: transparent; border: 1px solid var(--sv-accent); border-radius: 6px; transition: background 0.2s, color 0.2s, box-shadow 0.2s;
&:hover { color: var(--sv-gray-5); background: var(--sv-accent); box-shadow: 0 0 18px var(--sv-accent-glow); } } </style></sv-template>
<div class="content"></div>
<!-- Create cards --><script>89 collapsed lines
{ /** * @typedef {Object} CharacterData * @property {string} name * @property {string} icon * @property {string} [class] * @property {string} [element] * @property {string} [origin] * @property {number} hp * @property {number} attack * @property {number} [defense] * @property {string[]} skills * @property {string} [lore] * @property {boolean} [legendary] */
/** @type {CharacterData[]} */ const characters = [ { name: "Seraphel", icon: "🔥", class: "Arcane Knight", element: "fire", origin: "Dying sun of Astreth", hp: 88, attack: 95, defense: 72, skills: ["Solar Flare", "Ember Shield", "Ashen Blade"], lore: "Forged in the dying sun of Astreth, she carries its last ember.", legendary: true, }, { name: "Vorrk", icon: "🪨", class: "Stonewarden", element: "earth", hp: 99, attack: 60, defense: 97, skills: ["Granite Wall", "Tremor"], lore: "His shield has never been broken. His spirit never will.", }, { name: "Nyxara", icon: "🌑", class: "Shadow Dancer", element: "void", origin: "The Between", hp: 65, attack: 91, defense: 44, skills: ["Blink Strike", "Void Step", "Eclipse"], lore: "She moves between heartbeats, unseen until it's too late.", legendary: true, }, { name: "Thalion", icon: "⚡", element: "lightning", hp: 78, attack: 83, skills: ["Chain Lightning", "Static Dash"], }, { name: "Mireille", icon: "🌿", class: "Grove Sage", element: "nature", hp: 72, attack: 55, defense: 68, skills: ["Regrowth", "Thorn Bind", "Sporesong"], lore: "She speaks to roots older than memory.", }, { name: "Caldris", icon: "❄️", class: "Frost Arbiter", element: "ice", origin: "The Shattered Shelf", hp: 81, attack: 77, defense: 80, skills: ["Glacial Rift", "Permafrost", "Ice Mirror"], legendary: true, }, ];
/** @type {Template} */ const cardTemplate = document.querySelector("#card-template");
/** @type {HTMLElement} */ const content = document.querySelector(".content");
for (const charData of characters) { const templateInstance = cardTemplate.create(charData); content.append(templateInstance); } }</script>
<style>24 collapsed lines
.content { --gold: #c9a84c; --gold-glow: rgb(201 168 76 / 35%);
display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 2em; padding: 1em; margin: 0 auto; }
@keyframes pulse { 0%, 100% { opacity: 0.6; transform: scale(1); }
50% { opacity: 1; transform: scale(1.1); } }</style>