Added /dev/icons page for easy development
This commit is contained in:
@@ -47,6 +47,16 @@ export class AppRoot extends LitElement {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Dev ─────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
path: '/dev/icons',
|
||||||
|
auth: 'public',
|
||||||
|
view: async () => {
|
||||||
|
await import('./pages/dev/icons-page.js');
|
||||||
|
return document.createElement('dev-icons-page');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// ── Competition Center (User-Login) ──────────────────
|
// ── Competition Center (User-Login) ──────────────────
|
||||||
{
|
{
|
||||||
path: '/cc',
|
path: '/cc',
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { LitElement, html, css, type SVGTemplateResult } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators.js";
|
||||||
|
|
||||||
|
const iconModules = import.meta.glob<{ icon: SVGTemplateResult }>(
|
||||||
|
"../../components/icons/*.ts"
|
||||||
|
);
|
||||||
|
|
||||||
|
@customElement("dev-icons-page")
|
||||||
|
export class DevIconsPage extends LitElement {
|
||||||
|
@state() private icons: Array<{ name: string; icon: SVGTemplateResult }> = [];
|
||||||
|
@state() private search = "";
|
||||||
|
@state() private copied: string | null = null;
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--color-bg);
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: var(--color-bg-nav);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--color-text);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
background: var(--color-bg-nav);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-card:hover {
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 8%, var(--color-bg-nav));
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-card.copied-flash {
|
||||||
|
border-color: #22c55e;
|
||||||
|
background: color-mix(in srgb, #22c55e 8%, var(--color-bg-nav));
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-name {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-hint {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 6px;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
color: #22c55e;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-card.copied-flash .copy-hint {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
const entries = await Promise.all(
|
||||||
|
Object.entries(iconModules).map(async ([path, loader]) => {
|
||||||
|
const mod = await loader();
|
||||||
|
const name = path.replace(/.*\//, "").replace(/\.ts$/, "");
|
||||||
|
return { name, icon: mod.icon };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.icons = entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private get filtered() {
|
||||||
|
if (!this.search.trim()) return this.icons;
|
||||||
|
const q = this.search.trim().toLowerCase();
|
||||||
|
return this.icons.filter((i) => i.name.includes(q));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSearch(e: Event) {
|
||||||
|
this.search = (e.target as HTMLInputElement).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private copyName(name: string) {
|
||||||
|
navigator.clipboard.writeText(name);
|
||||||
|
this.copied = name;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.copied = null;
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const filtered = this.filtered;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<h1>Icon Library</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
${this.icons.length} icons — Klick auf ein Icon kopiert den Namen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="search-bar">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 15.803a7.5 7.5 0 0 0 10.607 0Z" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Icons durchsuchen…"
|
||||||
|
.value=${this.search}
|
||||||
|
@input=${this.handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="count">${filtered.length} Ergebnis${filtered.length !== 1 ? "se" : ""}</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
${filtered.length === 0
|
||||||
|
? html`<div class="empty">Kein Icon gefunden für „${this.search}"</div>`
|
||||||
|
: filtered.map(
|
||||||
|
({ name, icon }) => html`
|
||||||
|
<div
|
||||||
|
class="icon-card ${this.copied === name ? "copied-flash" : ""}"
|
||||||
|
title=${name}
|
||||||
|
@click=${() => this.copyName(name)}
|
||||||
|
>
|
||||||
|
<span class="copy-hint">kopiert!</span>
|
||||||
|
<div class="icon-wrapper">${icon}</div>
|
||||||
|
<span class="icon-name">${name}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user