diff --git a/src/app-root.ts b/src/app-root.ts index c507dde..ec7e10c 100644 --- a/src/app-root.ts +++ b/src/app-root.ts @@ -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) ────────────────── { path: '/cc', diff --git a/src/pages/dev/icons-page.ts b/src/pages/dev/icons-page.ts new file mode 100644 index 0000000..4a53abe --- /dev/null +++ b/src/pages/dev/icons-page.ts @@ -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` +
+ ${this.icons.length} icons — Klick auf ein Icon kopiert den Namen +
+ + + +