Compare commits
2 Commits
6b71a98a45
...
a1b90eb245
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1b90eb245 | ||
| b6512a318d |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -39,3 +39,10 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
.impeccable
|
||||
.codex
|
||||
.claude
|
||||
.agent
|
||||
.agents
|
||||
.claudeignore
|
||||
|
||||
209
DESIGN.md
Normal file
209
DESIGN.md
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
name: Entscheidomat
|
||||
description: Deutschsprachige Zufalls-Tools mit warmem Porzellan-Papier-Design und Smaragd-Akzenten
|
||||
colors:
|
||||
paper: "#faf8f3"
|
||||
surface: "#ffffff"
|
||||
ink: "#111412"
|
||||
ink-soft: "#45504a"
|
||||
line: "#e0eae4"
|
||||
emerald: "#0d7a57"
|
||||
emerald-strong: "#095c41"
|
||||
emerald-soft: "#e6f7f0"
|
||||
emerald-line: "#b2e5d1"
|
||||
clay: "#b4633f"
|
||||
clay-soft: "#f6eae2"
|
||||
typography:
|
||||
display:
|
||||
fontFamily: "Cormorant Garamond, Georgia, serif"
|
||||
fontSize: "clamp(2.5rem, 6vw, 4.5rem)"
|
||||
fontWeight: 600
|
||||
lineHeight: "1.1"
|
||||
letterSpacing: "-0.01em"
|
||||
headline:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "1.875rem"
|
||||
fontWeight: 700
|
||||
lineHeight: "1.2"
|
||||
letterSpacing: "-0.02em"
|
||||
title:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "1.25rem"
|
||||
fontWeight: 600
|
||||
lineHeight: "1.3"
|
||||
body:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "1rem"
|
||||
fontWeight: 400
|
||||
lineHeight: "1.6"
|
||||
label:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "0.75rem"
|
||||
fontWeight: 600
|
||||
lineHeight: "1"
|
||||
letterSpacing: "0.15em"
|
||||
rounded:
|
||||
sm: "8px"
|
||||
md: "16px"
|
||||
lg: "24px"
|
||||
full: "9999px"
|
||||
spacing:
|
||||
xs: "4px"
|
||||
sm: "8px"
|
||||
md: "16px"
|
||||
lg: "24px"
|
||||
xl: "32px"
|
||||
2xl: "48px"
|
||||
3xl: "64px"
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "{colors.emerald}"
|
||||
textColor: "{colors.surface}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: "12px 28px"
|
||||
button-primary-hover:
|
||||
backgroundColor: "{colors.emerald-strong}"
|
||||
button-secondary:
|
||||
backgroundColor: "{colors.surface}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: "12px 24px"
|
||||
card:
|
||||
backgroundColor: "{colors.surface}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: "24px"
|
||||
input:
|
||||
backgroundColor: "{colors.surface}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "12px 16px"
|
||||
---
|
||||
|
||||
# Design System: Entscheidomat
|
||||
|
||||
## Overview
|
||||
|
||||
**Creative North Star: "Das analoge Porzellan-Labor"**
|
||||
|
||||
Entscheidomat vereint die visuelle Wärme von tactilem Porzellan-Papier (`#faf8f3`) mit der Präzision eines modernen digitalen Werkzeugs. Statt lauter Kitsch-Visuals oder kalter SaaS-Gradients basiert das Design-System auf organischen, edlen Smaragd-Tönen (`#0d7a57`), feinen Begrenzungslinien (`#e0eae4`) und subtilen 3D-Zufallselementen (gebürstetes Messing für Münzen, mattes Anthrazit für Magic 8-Ball).
|
||||
|
||||
Das System legt höchsten Wert auf flüssiges visuelles Feedback, taktile Reaktion bei Klicks/Touches und eine ruhige, haptische Atmosphäre. Serifenstarke Akzente in *Cormorant Garamond* setzen spielerische Highlights im Markennamen und in Überschriften, während *Inter* für perfekte Lesbarkeit in Formularen, Tabellen und Interaktionsflächen sorgt.
|
||||
|
||||
**Key Characteristics:**
|
||||
- **Tactile Paper Canvas:** Warmes Porzellan-Papier (`#faf8f3`) als ruhiger, angenehmer Hintergrund für alle Seiten.
|
||||
- **Smaragd-Präzision:** Gezielter Einsatz von tiefem Smaragdgrün (`#0d7a57`) für primäre Aktionen, Erfolgszustände und Markenakzente.
|
||||
- **Typografischer Kontrast:** Zusammenspiel aus sachlicher Sans-Serif (*Inter*) und eleganter Display-Serif (*Cormorant Garamond*).
|
||||
- **Abgerundete Taktilität:** Vollabgerundete Pill-Buttons (`9999px`) für Aktionen und weiche Container-Ecken (`24px`).
|
||||
- **Subtile Tiefe:** Sanfte mehrschichtige Schatten statt schwerer Schlagschatten; Interaktionen erzeugen sanftes Anheben (*Lift*).
|
||||
|
||||
## Colors
|
||||
|
||||
Die Farbpalette ist warm, natürlich und fokussiert. Ein einzelner Hauptakzent in Smaragdgrün leitet den Blick, ergänzt durch einen warmen Terracotta-Farbton für seltene Sekundärakzente.
|
||||
|
||||
### Primary
|
||||
- **Porcelain Paper** (`#faf8f3`): Globaler Seitenhintergrund; schafft eine warme, papierartige Grundstimmung.
|
||||
- **Deep Ink** (`#111412`): Primäre Textfarbe und dunkle Bedienelemente für maximalen Kontrast.
|
||||
|
||||
### Accent
|
||||
- **Emerald Accent** (`#0d7a57`): Primärer Markenakzent; wird für CTA-Buttons, aktive Zustände und Markenhighlights verwendet.
|
||||
- **Emerald Strong** (`#095c41`): Hover- und Active-Zustand für Smaragd-Elemente.
|
||||
- **Emerald Soft Tint** (`#e6f7f0`): Sanfter Hintergrund-Tint für Tooltips, ausgewählte Elemente und Badges.
|
||||
- **Emerald Line** (`#b2e5d1`): Zarter Rahmen für Smaragd-Badges und Hover-Zustände.
|
||||
|
||||
### Secondary
|
||||
- **Terracotta Clay** (`#b4633f`): Warmer Sekundärakzent für ausgewählte Hervorhebungen und spielerische Akzente.
|
||||
- **Terracotta Soft Tint** (`#f6eae2`): Tint-Hintergrund für sekundäre Badges.
|
||||
|
||||
### Neutral
|
||||
- **Clean Surface** (`#ffffff`): Container-, Karten- und Modal-Hintergründe für klare Abhebung vom Paper-Background.
|
||||
- **Soft Ink** (`#45504a`): Sekundäre Textfarbe für Erklärungen, Untertitel und Metadaten.
|
||||
- **Warm Line** (`#e0eae4`): Subtile Trennlinien und Kartenränder.
|
||||
|
||||
### Named Rules
|
||||
**The Emerald Sparsensity Rule.** Der Smaragd-Akzent darf auf keiner Seite mehr als 10% der visuellen Fläche einnehmen. Seine Seltenheit verleiht ihm Wertigkeit und Leuchtkraft.
|
||||
|
||||
**The No-Pure-Black Rule.** Reines Schwarz (`#000000`) ist für Text und Flächen verboten. Stattdessen wird stets das warme Deep Ink (`#111412`) verwendet.
|
||||
|
||||
## Typography
|
||||
|
||||
Die Typografie basiert auf dem Kontrast zwischen der funktionalen Sans-Serif *Inter* und der charakterstarken Serif *Cormorant Garamond*.
|
||||
|
||||
**Display Font:** Cormorant Garamond, Georgia, serif
|
||||
**Body Font:** Inter, system-ui, -apple-system, sans-serif
|
||||
|
||||
### Hierarchy
|
||||
- **Display** (Semibold 600 / Italic, `clamp(2.5rem, 6vw, 4.5rem)`, Line-Height `1.1`): Hero-Überschriften und charakteristische Markenwörter (z. B. *omat* in *Entscheidomat*).
|
||||
- **Headline** (Bold 700, `1.875rem` / `30px`, Line-Height `1.2`, Letter-Spacing `-0.02em`): Sektionsüberschriften und Tool-Titel.
|
||||
- **Title** (Semibold 600, `1.25rem` / `20px`, Line-Height `1.3`): Kartenüberschriften und Dialog-Titel.
|
||||
- **Body** (Regular 400, `1rem` / `16px`, Line-Height `1.6`, max. Zeilenlänge 65–75ch): Fließtext, Beschreibungen und Anleitungen.
|
||||
- **Label** (Semibold 600, `0.75rem` / `12px`, Letter-Spacing `0.15em`, Uppercase): Eyebrows, Kategorie-Tags und Tabellenköpfe.
|
||||
|
||||
### Named Rules
|
||||
**The Dual-Type Rule.** *Cormorant Garamond* wird ausschließlich für Display-Akzente und dekorative Wort-Highlights verwendet; Fließtext, Buttons, Inputs und Navigations-Links verbleiben stets in *Inter*.
|
||||
|
||||
## Layout
|
||||
|
||||
Das Layout folgt einem großzügigen, zentrierten Rastersystem mit klaren Abständen und responsiver Skalierung.
|
||||
|
||||
- **Container:** Maximale Breite von `1152px` (`max-w-6xl`) mit horizontalem Padding (`px-5` mobil, `px-8` ab Desktop).
|
||||
- **Spatial Grid:** 8px-Rhythmus (`8px`, `16px`, `24px`, `32px`, `48px`, `64px`).
|
||||
- **Responsive Layout:** 1-spaltig auf Mobilgeräten, 2- bis 3-spaltige Grid-Layouts ab `md` (`768px`) und `lg` (`1024px`).
|
||||
- **Header:** Fixierte Navigation (`64px` Höhe) mit `backdrop-blur-xl` und fließendem Übergang beim Scrollen.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
Entscheidomat verwendet tonal geschichtetes Paper-on-Surface Design anstelle von schweren Schlagschatten. Tiefe entsteht primär durch den Kontrast zwischen Porzellan-Hintergrund (`#faf8f3`) und weißen Oberflächen (`#ffffff`), ergänzt durch sanfte, diffuse Umgebungsschatten.
|
||||
|
||||
### Shadow Vocabulary
|
||||
- **Soft Ambient** (`0 2px 4px rgb(17 20 18 / 0.03), 0 12px 36px rgb(17 20 18 / 0.06)`): Standard-Schatten für ruhende Karten und interaktive Container.
|
||||
- **Lifted Elevation** (`0 4px 12px rgb(17 20 18 / 0.06), 0 24px 52px rgb(17 20 18 / 0.10)`): Hover-Zustand für Karten und aktive Tool-Elemente.
|
||||
- **CTA Focus Glow** (`0 12px 28px rgb(17 20 18 / 0.22)`): Hervorhebung für primäre Aktionsbuttons.
|
||||
|
||||
### Named Rules
|
||||
**The Flat-at-Rest Rule.** Elemente ruhen flach mit minimalem Umgebungsleuchten (`Soft Ambient`). Tiefe entsteht dynamisch erst bei Benutzer-Interaktion (Hover/Focus).
|
||||
|
||||
## Shapes
|
||||
|
||||
Das Formensystem kombiniert organisch gerundete Ecken mit präzisen Kanten.
|
||||
|
||||
- **Buttons & Pills:** Vollabgerundet (`9999px`) für haptisches, freundliches Aussehen.
|
||||
- **Karten & Container:** `24px` Radius (`rounded-3xl` / `rounded-2xl`) für moderne Softness.
|
||||
- **Inputs & Formulare:** `16px` Radius (`rounded-xl`) mit feiner `1px` Umrandung (`#e0eae4`).
|
||||
- **Focus Rings:** `2px` durchgezogene Smaragd-/Ink-Linie mit `2px` Offset und `6px` Radius.
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
- **Shape:** Full Pill (`border-radius: 9999px`)
|
||||
- **Primary:** Background `#0d7a57`, Text `#ffffff`, Padding `12px 28px`, Font Weight 600.
|
||||
- **Primary Hover:** Background `#095c41`, Scale-up Transition `scale(1.02)`.
|
||||
- **Secondary:** Background `#ffffff`, Text `#111412`, Border `1px solid #e0eae4`, Shadow `Soft Ambient`.
|
||||
- **Sound/Icon Button:** Kreisförmig (`40px x 40px`), Background `#ffffff`, Border `#e0eae4`, Icon `#45504a`.
|
||||
|
||||
### Cards & Tool-Container
|
||||
- **Corner Style:** `24px` (`rounded-3xl`)
|
||||
- **Background:** `#ffffff` (`bg-surface`)
|
||||
- **Border:** `1px solid #e0eae4`
|
||||
- **Internal Padding:** `24px` bis `32px`
|
||||
- **Shadow:** `Soft Ambient` in Ruhe, transformiert zu `Lifted Elevation` bei Hover.
|
||||
|
||||
### Inputs / Text Fields
|
||||
- **Style:** Background `#ffffff`, Text `#111412`, Border `1px solid #e0eae4`, Radius `16px`.
|
||||
- **Focus:** Border-Color `#0d7a57`, Focus-Ring `2px solid #0d7a57` mit `2px` Offset.
|
||||
|
||||
### Navigation
|
||||
- **Style:** Fixierte Leiste (`h-16`), Transparent im Ruhezustand an der Spitze, wechselt beim Scrollen zu `bg-paper/95` mit `backdrop-blur-xl` und zartem unteren Rand (`#e0eae4`).
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do:
|
||||
- **Do** immer das warme Porzellan-Papier (`#faf8f3`) als Seitenhintergrund verwenden.
|
||||
- **Do** Smaragdgrün (`#0d7a57`) gezielt für die wichtigsten primären Interaktionen einsetzen.
|
||||
- **Do** Buttons stets als Abgerundete Pills (`rounded-full`) gestalten.
|
||||
- **Do** flüssige Framer-Motion / GSAP Übergänge bei Zufalls-Auslosungen anbieten.
|
||||
|
||||
### Don't:
|
||||
- **Don't** kühle bläuliche Grautöne oder schrill-leuchtende Gradienten verwenden.
|
||||
- **Don't** reines Schwarz (`#000000`) für Text oder Flächen nutzen.
|
||||
- **Don't** Gedankenstriche oder Bindestrich-Separaten in Eyebrows oder Titeln verwenden.
|
||||
- **Don't** harte Schlagschatten oder knallige Neonfarben einbauen.
|
||||
46
PRODUCT.md
Normal file
46
PRODUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Product
|
||||
|
||||
<!-- impeccable:product-schema 1 -->
|
||||
|
||||
## Platform
|
||||
|
||||
web
|
||||
|
||||
## Users
|
||||
Alltags- & Freizeitnutzer in spontanen Entscheidungssituationen (z. B. schnelle Ja/Nein-Entscheidungen, Spieleabende, Auswahl von Essen oder Freizeitaktivitäten) sowie kleinere Gruppen und Teams.
|
||||
|
||||
## Product Purpose
|
||||
Entscheidomat bietet eine Sammlung deutschsprachiger, kostenloser und intuitiver Zufalls-Tools (Ja/Nein-Generator, Münzwurf, Glücksrad, Würfel, Zufallszahl-Generator, Magic 8-Ball, Namen-Auslosung). Das Produkt nimmt Nutzenden spielerisch den Entscheidungsdruck ab und bringt Leichtigkeit in spontane Alltagssituationen.
|
||||
|
||||
## Positioning
|
||||
Maximale Flexibilität & Anpassbarkeit der einzelnen Zufalls-Tools kombiniert mit schnellen Ladezeiten, modernem visuellen Feedback und hoher Nutzerfreundlichkeit ohne Barrieren.
|
||||
|
||||
## Operating Context
|
||||
Spontane Nutzung auf Smartphone, Tablet oder Desktop-Browser im Alltagsgeschehen, bei Treffen mit Freunden, in der Freizeit oder bei der Tagesplanung.
|
||||
|
||||
## Capabilities and Constraints
|
||||
- **Funktionalität:** 7 Kern-Tools (Ja/Nein-Generator, Münze, Glücksrad, Würfel, Zufallszahl, Magic 8-Ball, Namen-Auslosung).
|
||||
- **Technischer Stack:** Next.js 16 (App Router), React 19, TypeScript, Tailwind CSS v4, Framer Motion & GSAP.
|
||||
- **Datenhaltung:** Keine Server-Datenbank; Einstellungsdaten und Anpassungen werden lokal im Browser (LocalStorage) gespeichert.
|
||||
- **Infrastructure:** Docker Multi-Stage Build, Caddy Reverse Proxy, Umami Cloud (EU-Region, anonyme Statistiken).
|
||||
- **Sprache:** Deutschsprachig.
|
||||
|
||||
## Brand Commitments
|
||||
- **Markenname:** Entscheidomat (`entscheidomat.com`)
|
||||
- **Tonalität & Voice:** Locker, sympathisch, klar & intuitiv.
|
||||
- **Qualitätsanspruch:** Kostenlos, schnell und barrierefrei nutzbar.
|
||||
|
||||
## Evidence on Hand
|
||||
- Funktionsfähige Web-App mit 7 integrierten Tool-Komponenten unter `components/tools/`
|
||||
- SEO-Vorbereitungen mit Open-Graph Previews, `sitemap.xml` und `robots.txt`
|
||||
- Vorhandene Dokumentation und Deployment-Guides (`README.md`, `ZUKUNFTSPLAN.md`)
|
||||
|
||||
## Product Principles
|
||||
1. **Sofortige Nutzbarkeit:** Keine Registrierung, keine Hürden – das gewählte Tool steht sofort zur Interaktion bereit.
|
||||
2. **Maximale Anpassbarkeit:** Hohe Flexibilität bei Eingaben, Namen, Listen und Einstellungen für unterschiedliche Einsatzzwecke.
|
||||
3. **Visuelle Leichtigkeit & Feedback:** Ansprechende Animationen und deutliches haptisches/visuelles Feedback machen das Auslosen unterhaltsam.
|
||||
4. **Datenschutz durch Design:** Keine Speicherung personenbezogener Daten; Einstellungen bleiben vollständig im Browser des Nutzers.
|
||||
|
||||
## Accessibility & Inclusion
|
||||
- Vollständige Tastatur- und Screenreader-Bedienbarkeit aller Tools.
|
||||
- WCAG AA Kontrastwerte und deutliche Fokus-Zustände.
|
||||
@@ -315,7 +315,7 @@ export default function Hero() {
|
||||
aria-label="Zu den Tools scrollen"
|
||||
className="absolute bottom-6 left-1/2 z-10 hidden -translate-x-1/2 flex-col items-center gap-1 text-ink-soft transition-colors hover:text-ink md:flex"
|
||||
>
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.2em]">
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.15em]">
|
||||
Scroll
|
||||
</span>
|
||||
<ArrowDown className="animate-bob h-4 w-4" />
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function LegalLayout({
|
||||
ladungsfähige Geschäftsanschrift vollständig ergänzt werden.
|
||||
</div>
|
||||
|
||||
<div className="mt-10 space-y-8 text-[15px] leading-relaxed text-ink-soft [&_h2]:font-display [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:text-ink [&_a]:text-[#0d7a57] [&_a]:underline [&_a]:underline-offset-2 [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5">
|
||||
<div className="mt-10 space-y-8 text-base leading-relaxed text-ink-soft [&_h2]:font-display [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:text-ink [&_a]:text-[#0d7a57] [&_a]:underline [&_a]:underline-offset-2 [&_ul]:list-disc [&_ul]:space-y-1 [&_ul]:pl-5">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function Nav() {
|
||||
</button>
|
||||
{toolsOpen && (
|
||||
<div role="menu" className="absolute right-0 top-[calc(100%+0.7rem)] w-80 overflow-hidden rounded-3xl border border-line bg-surface p-2 shadow-[0_22px_55px_rgba(17,20,18,0.16)]">
|
||||
<p className="px-3 pb-2 pt-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-ink-soft">Alle Zufalls-Tools</p>
|
||||
<p className="px-3 pb-2 pt-1 text-xs font-semibold uppercase tracking-[0.15em] text-ink-soft">Alle Zufalls-Tools</p>
|
||||
{TOOLS.map((tool) => (
|
||||
<Link key={tool.href} href={tool.href} role="menuitem" onClick={closeMenus} className="group flex items-center justify-between rounded-2xl px-3 py-2.5 transition-colors hover:bg-[#e6f7f0]">
|
||||
<span><span className="block text-sm font-semibold text-ink group-hover:text-[#0d7a57]">{tool.label}</span><span className="block text-xs text-ink-soft">{tool.hint}</span></span>
|
||||
@@ -121,7 +121,7 @@ export default function Nav() {
|
||||
{mobileOpen && (
|
||||
<div id="mobile-menu" className="border-t border-line/70 bg-paper px-5 pb-6 pt-4 shadow-xl sm:hidden">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<p className="mb-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-ink-soft">Tools auswählen</p>
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-[0.15em] text-ink-soft">Tools auswählen</p>
|
||||
<div className="grid gap-1">
|
||||
{TOOLS.map((tool) => <Link key={tool.href} href={tool.href} onClick={closeMenus} className="flex items-center justify-between rounded-2xl px-4 py-3 hover:bg-[#e6f7f0]"><span><span className="block font-semibold text-ink">{tool.label}</span><span className="text-xs text-ink-soft">{tool.hint}</span></span><span className="text-[#0d7a57]">→</span></Link>)}
|
||||
</div>
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function Playground() {
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-[#0d7a57]/20 bg-[#e6f7f0] text-[#0d7a57] shadow-sm transition-all duration-300 group-hover:scale-110 group-hover:border-[#0d7a57] group-hover:bg-[#0d7a57] group-hover:text-white">
|
||||
<Icon className="h-6 w-6" strokeWidth={2.1} />
|
||||
</div>
|
||||
<span className="inline-flex items-center rounded-full border border-line bg-paper px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-ink-soft">
|
||||
<span className="inline-flex items-center rounded-full border border-line bg-paper px-3 py-1 text-xs font-semibold uppercase tracking-wider text-ink-soft">
|
||||
{tool.badge}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -139,7 +139,7 @@ export default function Preloader() {
|
||||
</svg>
|
||||
<p
|
||||
ref={wordRef}
|
||||
className="mt-6 text-[11px] font-semibold uppercase tracking-[0.28em] text-ink-soft opacity-0"
|
||||
className="mt-6 text-xs font-semibold uppercase tracking-[0.2em] text-ink-soft opacity-0"
|
||||
>
|
||||
Entscheidomat
|
||||
</p>
|
||||
|
||||
@@ -130,9 +130,10 @@ export default function CoinFlip() {
|
||||
<input
|
||||
value={frontLabel}
|
||||
onChange={(e) => setFrontLabel(e.target.value)}
|
||||
disabled={flipping}
|
||||
maxLength={14}
|
||||
placeholder="Kopf"
|
||||
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
@@ -142,9 +143,10 @@ export default function CoinFlip() {
|
||||
<input
|
||||
value={backLabel}
|
||||
onChange={(e) => setBackLabel(e.target.value)}
|
||||
disabled={flipping}
|
||||
maxLength={14}
|
||||
placeholder="Zahl"
|
||||
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +208,8 @@ export default function DiceRoller() {
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => setMode(m.id)}
|
||||
className={`relative flex-1 cursor-pointer rounded-full px-3 py-2 text-sm font-semibold transition-colors duration-200 ${
|
||||
disabled={rolling}
|
||||
className={`relative flex-1 cursor-pointer rounded-full px-3 py-2 text-sm font-semibold transition-colors duration-200 disabled:cursor-not-allowed disabled:opacity-60 ${
|
||||
mode === m.id ? "text-white" : "text-ink-soft hover:text-ink"
|
||||
}`}
|
||||
>
|
||||
@@ -236,7 +237,7 @@ export default function DiceRoller() {
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
{customFaces.map((face, i) => (
|
||||
<label key={i} className="block">
|
||||
<span className="mb-1 block text-[11px] font-medium text-ink-soft">
|
||||
<span className="mb-1 block text-xs font-medium text-ink-soft">
|
||||
Seite {i + 1}
|
||||
</span>
|
||||
<input
|
||||
@@ -248,9 +249,10 @@ export default function DiceRoller() {
|
||||
),
|
||||
)
|
||||
}
|
||||
disabled={rolling}
|
||||
maxLength={18}
|
||||
placeholder={`Seite ${i + 1}`}
|
||||
className="w-full rounded-xl border border-line bg-surface px-3 py-2 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-xl border border-line bg-surface px-3 py-2 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
|
||||
@@ -20,7 +20,6 @@ export default function ListPicker() {
|
||||
const [display, setDisplay] = useState<string | null>(null);
|
||||
const [drawing, setDrawing] = useState(false);
|
||||
const [winner, setWinner] = useState<string | null>(null);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const lastPaintRef = useRef(0);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -32,10 +31,13 @@ export default function ListPicker() {
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const insufficientItems = items.length < 2;
|
||||
|
||||
const draw = () => {
|
||||
if (drawing) return;
|
||||
if (items.length < 2) {
|
||||
setError(true);
|
||||
if (insufficientItems) {
|
||||
setDisplay(null);
|
||||
setWinner(null);
|
||||
play("tick");
|
||||
gsap.fromTo(
|
||||
textareaRef.current,
|
||||
@@ -44,7 +46,6 @@ export default function ListPicker() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
setError(false);
|
||||
|
||||
const pick = items[Math.floor(Math.random() * items.length)];
|
||||
setDrawing(true);
|
||||
@@ -74,11 +75,16 @@ export default function ListPicker() {
|
||||
play("win");
|
||||
celebrateConfetti();
|
||||
if (removeAfterDraw) {
|
||||
setText(
|
||||
items
|
||||
.filter((item) => item !== pick)
|
||||
.join("\n"),
|
||||
);
|
||||
setText((prevText) => {
|
||||
const currentItems = prevText
|
||||
.split("\n")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const idx = currentItems.indexOf(pick);
|
||||
if (idx === -1) return prevText;
|
||||
currentItems.splice(idx, 1);
|
||||
return currentItems.join("\n");
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -95,11 +101,12 @@ export default function ListPicker() {
|
||||
ref={textareaRef}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={drawing}
|
||||
rows={8}
|
||||
placeholder={"Ein Eintrag pro Zeile, z. B.\nAnna\nBen\nClara"}
|
||||
aria-label="Liste der Einträge, ein Eintrag pro Zeile"
|
||||
className={`mt-4 w-full resize-none rounded-xl border bg-surface px-4 py-3 font-mono text-sm leading-relaxed outline-none transition focus:ring-2 ${
|
||||
error
|
||||
className={`mt-4 w-full resize-none rounded-xl border bg-surface px-4 py-3 font-mono text-sm leading-relaxed outline-none transition focus:ring-2 disabled:cursor-not-allowed disabled:opacity-60 ${
|
||||
insufficientItems
|
||||
? "border-clay/60 focus:border-clay/60 focus:ring-clay/20"
|
||||
: "border-line focus:border-primary/60 focus:ring-primary/20"
|
||||
}`}
|
||||
@@ -109,12 +116,13 @@ export default function ListPicker() {
|
||||
type="checkbox"
|
||||
checked={removeAfterDraw}
|
||||
onChange={(e) => setRemoveAfterDraw(e.target.checked)}
|
||||
className="h-4 w-4 cursor-pointer accent-primary"
|
||||
disabled={drawing}
|
||||
className="h-4 w-4 cursor-pointer accent-primary disabled:cursor-not-allowed"
|
||||
/>
|
||||
Gezogene Einträge entfernen (ohne Zurücklegen)
|
||||
</label>
|
||||
<p className={`mt-3 text-xs leading-relaxed ${error ? "font-semibold text-clay" : "text-ink-soft"}`}>
|
||||
{error
|
||||
<p className={`mt-3 text-xs leading-relaxed ${insufficientItems ? "font-semibold text-clay" : "text-ink-soft"}`}>
|
||||
{insufficientItems
|
||||
? "Bitte mindestens 2 Einträge eintragen."
|
||||
: "Ideal für Teams, Gruppen-Reihenfolgen oder Gewinnspiele."}
|
||||
</p>
|
||||
@@ -133,7 +141,7 @@ export default function ListPicker() {
|
||||
>
|
||||
<ListChecks className="h-8 w-8 text-primary/50" />
|
||||
<p className="font-display text-xl font-medium text-ink-soft">
|
||||
{items.length >= 2
|
||||
{!insufficientItems
|
||||
? `${items.length} Einträge warten auf die Ziehung.`
|
||||
: "Trage links mindestens 2 Einträge ein."}
|
||||
</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Pencil, Plus, Sparkles, X } from "lucide-react";
|
||||
import { gsap } from "@/lib/gsap";
|
||||
@@ -36,6 +36,8 @@ export default function Magic8Ball() {
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
const ballRef = useRef<HTMLDivElement>(null);
|
||||
const answerInputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
const justAddedRef = useRef(false);
|
||||
const { play } = useSound();
|
||||
const reduced = usePrefersReducedMotion();
|
||||
|
||||
@@ -90,8 +92,17 @@ export default function Magic8Ball() {
|
||||
const addAnswer = () => {
|
||||
if (answers.length >= MAX_ANSWERS) return;
|
||||
setAnswers((list) => [...list, ""]);
|
||||
justAddedRef.current = true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!justAddedRef.current) return;
|
||||
justAddedRef.current = false;
|
||||
const last = answerInputRefs.current[answers.length - 1];
|
||||
last?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
last?.focus();
|
||||
}, [answers.length]);
|
||||
|
||||
const removeAnswer = (i: number) => {
|
||||
if (answers.length <= MIN_ANSWERS) return;
|
||||
setAnswers((list) => list.filter((_, idx) => idx !== i));
|
||||
@@ -138,7 +149,7 @@ export default function Magic8Ball() {
|
||||
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, filter: "blur(6px)" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
className="text-center font-display text-[13px] font-semibold leading-snug text-[#dde5dd]"
|
||||
className="text-center font-display text-sm font-semibold leading-snug text-[#dde5dd]"
|
||||
>
|
||||
{answer}
|
||||
</motion.p>
|
||||
@@ -210,6 +221,9 @@ export default function Magic8Ball() {
|
||||
{answers.map((a, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<input
|
||||
ref={(el) => {
|
||||
answerInputRefs.current[i] = el;
|
||||
}}
|
||||
value={a}
|
||||
onChange={(e) => updateAnswer(i, e.target.value)}
|
||||
maxLength={60}
|
||||
|
||||
@@ -12,6 +12,11 @@ import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion";
|
||||
const randInt = (lo: number, hi: number) =>
|
||||
lo + Math.floor(Math.random() * (hi - lo + 1));
|
||||
|
||||
const toInt = (raw: string) => {
|
||||
const n = Math.round(Number(raw));
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
};
|
||||
|
||||
export default function RandomNumber() {
|
||||
const [min, setMin] = useLocalStorage("num-min", 1);
|
||||
const [max, setMax] = useLocalStorage("num-max", 100);
|
||||
@@ -120,9 +125,11 @@ export default function RandomNumber() {
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step={1}
|
||||
value={min}
|
||||
onChange={(e) => setMin(Number(e.target.value))}
|
||||
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
|
||||
onChange={(e) => setMin(toInt(e.target.value))}
|
||||
disabled={rolling}
|
||||
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
@@ -131,9 +138,11 @@ export default function RandomNumber() {
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
step={1}
|
||||
value={max}
|
||||
onChange={(e) => setMax(Number(e.target.value))}
|
||||
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
|
||||
onChange={(e) => setMax(toInt(e.target.value))}
|
||||
disabled={rolling}
|
||||
className="tabular w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Plus, RotateCcw, Sparkles, X } from "lucide-react";
|
||||
import { gsap } from "@/lib/gsap";
|
||||
@@ -51,6 +51,8 @@ export default function WheelOfFortune() {
|
||||
const pointerRef = useRef<HTMLDivElement>(null);
|
||||
const rotationRef = useRef(0);
|
||||
const lastTickRef = useRef(-1);
|
||||
const optionInputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
const justAddedRef = useRef(false);
|
||||
|
||||
const { play } = useSound();
|
||||
const reduced = usePrefersReducedMotion();
|
||||
@@ -115,20 +117,49 @@ export default function WheelOfFortune() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const activeTag = document.activeElement?.tagName.toLowerCase();
|
||||
if (activeTag === "input" || activeTag === "textarea") return;
|
||||
if (e.code === "Space") {
|
||||
e.preventDefault();
|
||||
spin();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [spinning, n]);
|
||||
|
||||
const updateOption = (i: number, value: string) => {
|
||||
if (spinning) return;
|
||||
setOptions((opts) => opts.map((o, idx) => (idx === i ? value : o)));
|
||||
setWinner(null);
|
||||
};
|
||||
|
||||
const addOption = () => {
|
||||
if (n >= MAX_OPTIONS) return;
|
||||
if (spinning || n >= MAX_OPTIONS) return;
|
||||
setOptions((opts) => [...opts, ""]);
|
||||
setWinner(null);
|
||||
justAddedRef.current = true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!justAddedRef.current) return;
|
||||
justAddedRef.current = false;
|
||||
const last = optionInputRefs.current[options.length - 1];
|
||||
last?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
last?.focus();
|
||||
}, [options.length]);
|
||||
|
||||
const removeOption = (i: number) => {
|
||||
if (spinning || n <= MIN_OPTIONS) return;
|
||||
setOptions((opts) => opts.filter((_, idx) => idx !== i));
|
||||
setWinner(null);
|
||||
};
|
||||
|
||||
const removeOption = (i: number) => {
|
||||
if (n <= MIN_OPTIONS) return;
|
||||
setOptions((opts) => opts.filter((_, idx) => idx !== i));
|
||||
const resetOptions = () => {
|
||||
if (spinning) return;
|
||||
setOptions(DEFAULT_OPTIONS);
|
||||
setWinner(null);
|
||||
};
|
||||
|
||||
@@ -141,14 +172,19 @@ export default function WheelOfFortune() {
|
||||
<div className="relative w-full max-w-[400px]">
|
||||
{/* Zeiger */}
|
||||
<div
|
||||
ref={pointerRef}
|
||||
ref={pointerRef}
|
||||
aria-hidden
|
||||
className="absolute -top-1.5 left-1/2 z-10 h-9 w-7 -translate-x-1/2 bg-primary shadow-[0_6px_16px_rgb(108_92_231/0.5)]"
|
||||
className="absolute -top-1.5 left-1/2 z-10 h-9 w-7 -translate-x-1/2 bg-primary shadow-[0_6px_16px_rgb(13_122_87/0.35)]"
|
||||
style={{
|
||||
clipPath: "polygon(50% 100%, 0 0, 100% 0)",
|
||||
}}
|
||||
/>
|
||||
<svg viewBox="0 0 420 420" className="w-full drop-shadow-[0_20px_36px_rgb(23_23_28/0.14)]">
|
||||
<svg
|
||||
viewBox="0 0 420 420"
|
||||
role="img"
|
||||
aria-label="Glücksrad mit Zufalls-Optionen"
|
||||
className="w-full drop-shadow-[0_20px_36px_rgb(23_23_28/0.14)]"
|
||||
>
|
||||
{/* äußerer Ring */}
|
||||
<circle cx={C} cy={C} r={206} fill="#ffffff" stroke="#e9e9e4" />
|
||||
<g ref={wheelRef}>
|
||||
@@ -199,7 +235,7 @@ export default function WheelOfFortune() {
|
||||
</g>
|
||||
{/* Nabe */}
|
||||
<circle cx={C} cy={C} r={30} fill="#ffffff" stroke="#e9e9e4" strokeWidth={2} />
|
||||
<circle cx={C} cy={C} r={9} fill="#6c5ce7" />
|
||||
<circle cx={C} cy={C} r={9} fill="#0d7a57" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
@@ -234,18 +270,16 @@ export default function WheelOfFortune() {
|
||||
</div>
|
||||
|
||||
{/* Optionen-Editor */}
|
||||
<div className="rounded-3xl border border-line bg-cream/70 p-5">
|
||||
<div className="rounded-3xl border border-line bg-surface p-5 shadow-soft">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-[0.16em] text-ink-soft">
|
||||
Optionen ({n}/{MAX_OPTIONS})
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOptions(DEFAULT_OPTIONS);
|
||||
setWinner(null);
|
||||
}}
|
||||
className="cursor-pointer text-xs font-medium text-ink-soft underline-offset-2 transition-colors hover:text-primary hover:underline"
|
||||
onClick={resetOptions}
|
||||
disabled={spinning}
|
||||
className="cursor-pointer text-xs font-medium text-ink-soft underline-offset-2 transition-colors hover:text-[#0d7a57] hover:underline disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Zurücksetzen
|
||||
</button>
|
||||
@@ -260,17 +294,21 @@ export default function WheelOfFortune() {
|
||||
style={{ background: PALETTE[i % PALETTE.length] }}
|
||||
/>
|
||||
<input
|
||||
ref={(el) => {
|
||||
optionInputRefs.current[i] = el;
|
||||
}}
|
||||
value={option}
|
||||
onChange={(e) => updateOption(i, e.target.value)}
|
||||
disabled={spinning}
|
||||
maxLength={24}
|
||||
placeholder={`Option ${i + 1}`}
|
||||
aria-label={`Option ${i + 1}`}
|
||||
className="w-full rounded-xl border border-line bg-surface px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-primary/60 focus:ring-2 focus:ring-primary/20"
|
||||
className="w-full rounded-xl border border-line bg-paper/50 px-3.5 py-2.5 text-sm font-medium outline-none transition focus:border-[#0d7a57] focus:bg-surface focus:ring-2 focus:ring-[#0d7a57]/20 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeOption(i)}
|
||||
disabled={n <= MIN_OPTIONS}
|
||||
disabled={spinning || n <= MIN_OPTIONS}
|
||||
aria-label={`Option ${i + 1} entfernen`}
|
||||
className="flex h-9 w-9 shrink-0 cursor-pointer items-center justify-center rounded-xl text-ink-soft transition-colors hover:bg-clay-soft hover:text-clay disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
@@ -283,8 +321,8 @@ export default function WheelOfFortune() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={addOption}
|
||||
disabled={n >= MAX_OPTIONS}
|
||||
className="mt-3 inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl border-2 border-dashed border-line px-4 py-2.5 text-sm font-semibold text-ink-soft transition-colors hover:border-primary/50 hover:text-primary disabled:cursor-not-allowed disabled:opacity-40"
|
||||
disabled={spinning || n >= MAX_OPTIONS}
|
||||
className="mt-3 inline-flex w-full cursor-pointer items-center justify-center gap-2 rounded-xl border-2 border-dashed border-line px-4 py-2.5 text-sm font-semibold text-ink-soft transition-colors hover:border-[#0d7a57] hover:bg-[#e6f7f0]/50 hover:text-[#0d7a57] disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Option hinzufügen
|
||||
|
||||
83
skills-lock.json
Normal file
83
skills-lock.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"brandkit": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/brandkit/SKILL.md",
|
||||
"computedHash": "735007879f5110aeae1e981a53fe74a12afde70347f9773dacd50e4697a091bf"
|
||||
},
|
||||
"design-taste-frontend": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/taste-skill/SKILL.md",
|
||||
"computedHash": "899b84384f74f540ea5284d9b2e9234e050998b42eacc805410b518d4226c0b3"
|
||||
},
|
||||
"design-taste-frontend-v1": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/taste-skill-v1/SKILL.md",
|
||||
"computedHash": "0a226c13d69639553cf8a7c40aa5527b026820f8ddaf7a6b263988e32ab44a27"
|
||||
},
|
||||
"full-output-enforcement": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/output-skill/SKILL.md",
|
||||
"computedHash": "524f7c7950c3fde726101613f630140411c7d080e63be718687f4b38f919be81"
|
||||
},
|
||||
"gpt-taste": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/gpt-tasteskill/SKILL.md",
|
||||
"computedHash": "7bd1bfdda0e51016ca222693401a65fc0dd9bf9487e0bd5b1740ba510f6f963a"
|
||||
},
|
||||
"high-end-visual-design": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/soft-skill/SKILL.md",
|
||||
"computedHash": "f730e4132775f13eea19e3dc39afc6bb453cfc0498872b127ad8f0d47cfd802d"
|
||||
},
|
||||
"image-to-code": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/image-to-code-skill/SKILL.md",
|
||||
"computedHash": "3d2ac14d956fdafc5500d2108b90895e134d847f9f199e7f1d18bf1f814cf7c8"
|
||||
},
|
||||
"imagegen-frontend-mobile": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/imagegen-frontend-mobile/SKILL.md",
|
||||
"computedHash": "7c1d1dc59cd0df4063b8621af543bf5dea1d576b261507e17b0323c69680b048"
|
||||
},
|
||||
"imagegen-frontend-web": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/imagegen-frontend-web/SKILL.md",
|
||||
"computedHash": "27255c7c50181e58add156b2aa2bd5325285b85f04027e4c8b73742d98d7be0a"
|
||||
},
|
||||
"industrial-brutalist-ui": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/brutalist-skill/SKILL.md",
|
||||
"computedHash": "e66e5a2a8b73cef79ab6546459c665e8b983645689c5f3772f7eed8b9f1e8e0e"
|
||||
},
|
||||
"minimalist-ui": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/minimalist-skill/SKILL.md",
|
||||
"computedHash": "7e88897e0371184e97a82b8d9ef080323c13f21eb067b58ef71eb05173a08ac2"
|
||||
},
|
||||
"redesign-existing-projects": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/redesign-skill/SKILL.md",
|
||||
"computedHash": "47eced3e960a2c961d3cbee11fa1216264f589344b586a5cbd03967cd6b9c54b"
|
||||
},
|
||||
"stitch-design-taste": {
|
||||
"source": "Leonxlnx/taste-skill",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/stitch-skill/SKILL.md",
|
||||
"computedHash": "c96b580192b34167efa01f460bbc669ff4315cf97f3cf6ff85ded8029aa720b8"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user