Name Generator Rework

This commit is contained in:
2025-08-01 01:13:33 +01:00
parent af9e88c580
commit 6075472f3d
3 changed files with 812 additions and 317 deletions
+96
View File
@@ -0,0 +1,96 @@
# NameGen API Documentation
## Base URL
```
https://namegen.fernandovideira.com/v1
```
## Fantasy Name Endpoints
| Race/Type | Endpoint | Query Parameters |
| ------------ | --------------------------- | ---------------- | ------ | ------- |
| Elf | /fantasy/elf-names | gender=male | female | child |
| Dwarf | /fantasy/dwarf-names | gender=male | female |
| Arakocra | /fantasy/arakocra-names | - |
| Aasimar | /fantasy/aasimar-names | gender=male | female |
| Bugbear | /fantasy/bugbear-names | - |
| Centaur | /fantasy/centaur-names | gender=male | female |
| Changeling | /fantasy/changeling-names | - |
| Deep Gnome | /fantasy/deep-gnome-names | gender=male | female |
| Deva | /fantasy/deva-names | gender=male | female |
| Dragonborn | /fantasy/dragonborn-names | gender=male | female | surname |
| Drow | /fantasy/drow-names | gender=male | female | surname |
| Duergar | /fantasy/duergar-names | gender=male | female |
| Eladrin | /fantasy/eladrin-names | gender=male | female |
| Fairy | /fantasy/fairy-names | gender=male | female |
| Firbolg | /fantasy/firbolg-names | gender=male | female | child |
| Genasi | /fantasy/genasi-names | - |
| Githyanki | /fantasy/githyanki-names | gender=male | female |
| Githzerai | /fantasy/githzerai-names | gender=male | female |
| Gnome | /fantasy/gnome-names | gender=male | female |
| Goblin | /fantasy/goblin-names | gender=male | female |
| Goliath | /fantasy/goliath-names | gender=male | female |
| Grung | /fantasy/grung-names | - |
| Half-Elf | /fantasy/half-elf-names | gender=male | female |
| Half-Orc | /fantasy/half-orc-names | gender=male | female |
| Halfling | /fantasy/halfling-names | gender=male | female |
| Harengon | /fantasy/harengon-names | gender=male | female |
| Hobgoblin | /fantasy/hobgoblin-names | - |
| Human | /fantasy/human-names | gender=male | female |
| Kalashtar | /fantasy/kalashtar-names | - |
| Kenku | /fantasy/kenku-names | - |
| Kobold | /fantasy/kobold-names | - |
| Leonin | /fantasy/leonin-names | gender=male | female |
| Lizardfolk | /fantasy/lizardfolk-names | gender=male | female |
| Locathah | /fantasy/locathah-names | - |
| Loxodon | /fantasy/loxodon-names | gender=male | female |
| Minotaur | /fantasy/minotaur-names | gender=male | female |
| Orc | /fantasy/orc-names | gender=male | female |
| Satyr | /fantasy/satyr-names | gender=male | female | child |
| Shadar-Kai | /fantasy/shadar-kai-names | gender=male | female |
| Shardmind | /fantasy/shardmind-names | - |
| Shifter | /fantasy/shifter-names | gender=male | female |
| Simic Hybrid | /fantasy/simic-hybrid-names | gender=male | female |
| Tabaxi | /fantasy/tabaxi-names | gender=male | female |
| Tiefling | /fantasy/tiefling-names | gender=male | female | virtue |
| Tortle | /fantasy/tortle-names | - |
| Triton | /fantasy/triton-names | gender=male | female |
| Vedalken | /fantasy/vedalken-names | gender=male | female |
| Verdan | /fantasy/verdan-names | - |
| Warforged | /fantasy/warforged-names | - |
| Wilden | /fantasy/wilden-names | gender=male | female |
| Yuan-Ti | /fantasy/yuan-ti-names | - |
## Modern Name Endpoints
| Language/Type | Endpoint | Query Parameters |
| ---------------------- | ------------------ | ---------------- | ------ | ------ |
| Modern English | /modern/english | gender=male | female | unisex |
| Modern Portuguese (EU) | /modern/portuguese | gender=male | female | unisex |
## How to Query the API
- All endpoints return a JSON object with a `names` array.
- Use the `gender` query parameter where supported (e.g., `?gender=female`).
- Example request:
```
GET https://namegen.fernandovideira.com/v1/fantasy/elf-names?gender=female
```
```
GET https://namegen.fernandovideira.com/v1/modern/english?gender=unisex
```
## Example Response
```json
{
"names": [
"Aelene Galanodel",
"Lia Siannodel",
...
]
}
```
+198 -317
View File
@@ -1,337 +1,218 @@
<template> <template>
<div class="name-generator"> <div class="name-generator-layout">
<h2>Name Generator</h2> <div class="page-header">
<div class="controls"> <h2>Name Generator</h2>
<div class="name-type-switch"> <div class="page-controls">
<span class="switch-label">Name Type:</span> <button
<div class="switch-container"> class="button-secondary"
<button @click="addGenerator"
type="button" :disabled="generators.length >= 6"
class="switch-button"
:class="{ active: !modern }"
@click="modern = false"
>
Fantasy
</button>
<button
type="button"
class="switch-button button-switch"
:class="{ active: modern }"
@click="modern = true"
>
Modern
</button>
</div>
</div>
<label v-if="!modern">
Race:
<select v-model="race">
<option v-for="r in races" :key="r.value" :value="r.value">
{{ r.label }}
</option>
</select>
</label>
<label>
Gender:
<select v-model="gender">
<option value="male">Male</option>
<option value="female">Female</option>
</select>
</label>
<label>
Count:
<select v-model="count">
<option :value="1">1 name</option>
<option :value="3">3 names</option>
<option :value="5">5 names</option>
<option :value="8">8 names</option>
<option :value="10">10 names</option>
<option :value="15">15 names</option>
<option :value="20">20 names</option>
</select>
</label>
</div>
<button class="button-main" @click="generate">Generate</button>
<ul class="results">
<li
v-for="name in fullNames"
:key="name"
@click="copyToClipboard(name)"
class="name-item"
:title="'Click to copy: ' + name"
>
{{ name }}
<svg
class="copy-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
> >
<rect <svg
x="9" width="16"
y="9" height="16"
width="13" viewBox="0 0 24 24"
height="13"
rx="2"
ry="2"
stroke="currentColor"
stroke-width="2"
fill="none" fill="none"
/> xmlns="http://www.w3.org/2000/svg"
<path >
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" <line
stroke="currentColor" x1="12"
stroke-width="2" y1="5"
x2="12"
y2="19"
stroke="currentColor"
stroke-width="2"
/>
<line
x1="5"
y1="12"
x2="19"
y2="12"
stroke="currentColor"
stroke-width="2"
/>
</svg>
Add Generator
</button>
<button
class="button-secondary"
@click="removeGenerator"
:disabled="generators.length <= 1"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none" fill="none"
/> xmlns="http://www.w3.org/2000/svg"
</svg> >
</li> <line
</ul> x1="5"
y1="12"
x2="19"
y2="12"
stroke="currentColor"
stroke-width="2"
/>
</svg>
Remove Generator
</button>
</div>
</div>
<!-- Toast notification --> <div class="generators-grid" :class="`grid-${generators.length}`">
<div v-if="showToast" class="toast"> <NameGeneratorCard v-for="id in generators" :key="id" :card-id="id" />
{{ toastMessage }}
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import NameGeneratorCard from "./NameGeneratorCard.vue";
const modern = ref(false); const generators = ref([1, 2, 3, 4]);
const races = [ let nextId = 5;
{ label: "Dragonborn", value: "dragonborn" },
{ label: "Dwarf", value: "dwarf" },
{ label: "Elf", value: "elf" },
{ label: "Gnome", value: "gnome" },
{ label: "Goblin", value: "goblin" },
{ label: "Half-Elf", value: "half-elf" },
{ label: "Half-Orc", value: "half-orc" },
{ label: "Halfling", value: "halfling" },
{ label: "Human", value: "human" },
{ label: "Orc", value: "orc" },
{ label: "Tiefling", value: "tiefling" },
{ label: "Troll", value: "troll" },
];
// Modern name arrays function addGenerator() {
const modernMaleNames = [ if (generators.value.length < 6) {
"James", generators.value.push(nextId++);
"John",
"Robert",
"Michael",
"William",
"David",
"Richard",
"Charles",
"Joseph",
"Thomas",
"Christopher",
"Daniel",
"Paul",
"Mark",
"Donald",
"Steven",
"Andrew",
"Kenneth",
"Joshua",
"Kevin",
"Brian",
"George",
"Timothy",
"Ronald",
"Jason",
"Edward",
"Jeffrey",
"Ryan",
"Jacob",
"Gary",
"Nicholas",
"Eric",
"Jonathan",
"Stephen",
"Larry",
"Justin",
"Scott",
"Brandon",
"Benjamin",
"Samuel",
];
const modernFemaleNames = [
"Mary",
"Patricia",
"Jennifer",
"Linda",
"Elizabeth",
"Barbara",
"Susan",
"Jessica",
"Sarah",
"Karen",
"Nancy",
"Lisa",
"Betty",
"Helen",
"Sandra",
"Donna",
"Carol",
"Ruth",
"Sharon",
"Michelle",
"Laura",
"Sarah",
"Kimberly",
"Deborah",
"Dorothy",
"Lisa",
"Nancy",
"Karen",
"Betty",
"Helen",
"Sandra",
"Donna",
"Carol",
"Ruth",
"Sharon",
"Michelle",
"Laura",
"Amy",
"Angela",
"Brenda",
];
const modernLastNames = [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Hernandez",
"Lopez",
"Gonzalez",
"Wilson",
"Anderson",
"Thomas",
"Taylor",
"Moore",
"Jackson",
"Martin",
"Lee",
"Perez",
"Thompson",
"White",
"Harris",
"Sanchez",
"Clark",
"Ramirez",
"Lewis",
"Robinson",
"Walker",
"Young",
"Allen",
"King",
"Wright",
"Scott",
"Torres",
"Nguyen",
"Hill",
"Flores",
"Green",
"Adams",
"Nelson",
"Baker",
"Hall",
"Rivera",
"Campbell",
"Mitchell",
"Carter",
"Roberts",
];
const race = ref(races[0].value);
const gender = ref("male");
const count = ref(5);
const fullNames = ref([]);
const showToast = ref(false);
const toastMessage = ref("");
function getRandomName(array) {
return array[Math.floor(Math.random() * array.length)];
}
function generateModernNames() {
const names = [];
const firstNames =
gender.value === "male" ? modernMaleNames : modernFemaleNames;
for (let i = 0; i < count.value; i++) {
const firstName = getRandomName(firstNames);
const lastName = getRandomName(modernLastNames);
names.push(`${firstName} ${lastName}`);
}
return names;
}
async function generate() {
fullNames.value = [];
if (modern.value) {
// Generate modern names locally
fullNames.value = generateModernNames();
} else {
// Use API for fantasy names
const selectedRace = race.value;
const firstUrl = `https://names.ironarachne.com/race/${selectedRace}/${gender.value}/${count.value}`;
const familyUrl = `https://names.ironarachne.com/race/${selectedRace}/family/${count.value}`;
try {
const [firstRes, familyRes] = await Promise.all([
fetch(firstUrl),
fetch(familyUrl),
]);
const firstData = await firstRes.json();
const familyData = await familyRes.json();
fullNames.value = firstData.names.map(
(n, i) => `${n} ${familyData.names[i]}`
);
} catch (e) {
console.error("Error fetching names", e);
}
} }
} }
async function copyToClipboard(name) { function removeGenerator() {
try { if (generators.value.length > 1) {
await navigator.clipboard.writeText(name); generators.value.pop();
showToastMessage(`"${name}" copied to clipboard!`);
} catch (err) {
console.error("Failed to copy to clipboard:", err);
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = name;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
showToastMessage(`"${name}" copied to clipboard!`);
} }
} }
function showToastMessage(message) {
toastMessage.value = message;
showToast.value = true;
setTimeout(() => {
showToast.value = false;
}, 2000);
}
</script> </script>
<style scoped src="./NameGenerator.css"></style> <style scoped>
.name-generator-layout {
padding: var(--spacing-lg);
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border);
}
.page-header h2 {
margin: 0;
color: var(--text-primary);
font-size: 2rem;
}
.page-controls {
display: flex;
gap: var(--spacing-sm);
}
.button-secondary {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--surface);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.button-secondary:hover:not(:disabled) {
background: var(--surface-hover);
border-color: var(--border-hover);
transform: translateY(-1px);
}
.button-secondary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.generators-grid {
display: grid;
gap: var(--spacing-lg);
width: 100%;
}
.grid-1 {
grid-template-columns: 1fr;
max-width: 600px;
margin: 0 auto;
}
.grid-2 {
grid-template-columns: repeat(2, 1fr);
}
.grid-3 {
grid-template-columns: repeat(3, 1fr);
}
.grid-4 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.grid-5 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.grid-6 {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
}
/* Responsive design */
@media (max-width: 1024px) {
.grid-3,
.grid-4,
.grid-5,
.grid-6 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: auto;
}
}
@media (max-width: 768px) {
.name-generator-layout {
padding: var(--spacing-md);
}
.page-header {
flex-direction: column;
gap: var(--spacing-md);
align-items: stretch;
}
.page-controls {
justify-content: center;
}
.generators-grid {
grid-template-columns: 1fr !important;
grid-template-rows: auto !important;
}
}
@media (max-width: 480px) {
.page-controls {
flex-direction: column;
}
.button-secondary {
justify-content: center;
}
}
</style>
+518
View File
@@ -0,0 +1,518 @@
<template>
<div class="name-generator-card">
<div class="card-header">
<h3>Generator {{ cardId }}</h3>
<button class="reset-btn" @click="resetGenerator" title="Reset Generator">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
<path
d="M21 3v5h-5"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
<path
d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
<path
d="M3 21v-5h5"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
</svg>
</button>
</div>
<div class="controls">
<div class="name-type-switch">
<span class="switch-label">Type:</span>
<div class="switch-container">
<button
type="button"
class="switch-button"
:class="{ active: !modern }"
@click="modern = false"
>
Fantasy
</button>
<button
type="button"
class="switch-button button-switch"
:class="{ active: modern }"
@click="modern = true"
>
Modern
</button>
</div>
</div>
<label v-if="!modern">
Race:
<select v-model="race">
<option v-for="r in races" :key="r.value" :value="r.value">
{{ r.label }}
</option>
</select>
</label>
<label v-if="modern">
Language:
<select v-model="language">
<option value="english">English</option>
<option value="portuguese">Portuguese (EU)</option>
</select>
</label>
<label>
Gender:
<select v-model="gender">
<option value="male">Male</option>
<option value="female">Female</option>
<option v-if="modern" value="unisex">Unisex</option>
</select>
</label>
</div>
<button class="button-generate" @click="generate" :disabled="isLoading">
{{ isLoading ? "Generating..." : "Generate" }}
</button>
<ul class="results" v-if="fullNames.length > 0">
<li
v-for="name in fullNames"
:key="name"
@click="copyToClipboard(name)"
class="name-item"
:title="'Click to copy: ' + name"
>
{{ name }}
<svg
class="copy-icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="9"
y="9"
width="13"
height="13"
rx="2"
ry="2"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
<path
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
</svg>
</li>
</ul>
<!-- Toast notification -->
<div v-if="showToast" class="toast">
{{ toastMessage }}
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const props = defineProps({
cardId: {
type: Number,
required: true,
},
});
const modern = ref(false);
const isLoading = ref(false);
const races = [
{ label: "Aasimar", value: "aasimar" },
{ label: "Arakocra", value: "arakocra" },
{ label: "Bugbear", value: "bugbear" },
{ label: "Centaur", value: "centaur" },
{ label: "Changeling", value: "changeling" },
{ label: "Deep Gnome", value: "deep-gnome" },
{ label: "Deva", value: "deva" },
{ label: "Dragonborn", value: "dragonborn" },
{ label: "Drow", value: "drow" },
{ label: "Duergar", value: "duergar" },
{ label: "Dwarf", value: "dwarf" },
{ label: "Eladrin", value: "eladrin" },
{ label: "Elf", value: "elf" },
{ label: "Fairy", value: "fairy" },
{ label: "Firbolg", value: "firbolg" },
{ label: "Genasi", value: "genasi" },
{ label: "Githyanki", value: "githyanki" },
{ label: "Githzerai", value: "githzerai" },
{ label: "Gnome", value: "gnome" },
{ label: "Goblin", value: "goblin" },
{ label: "Goliath", value: "goliath" },
{ label: "Grung", value: "grung" },
{ label: "Half-Elf", value: "half-elf" },
{ label: "Half-Orc", value: "half-orc" },
{ label: "Halfling", value: "halfling" },
{ label: "Harengon", value: "harengon" },
{ label: "Hobgoblin", value: "hobgoblin" },
{ label: "Human", value: "human" },
{ label: "Kalashtar", value: "kalashtar" },
{ label: "Kenku", value: "kenku" },
{ label: "Kobold", value: "kobold" },
{ label: "Leonin", value: "leonin" },
{ label: "Lizardfolk", value: "lizardfolk" },
{ label: "Locathah", value: "locathah" },
{ label: "Loxodon", value: "loxodon" },
{ label: "Minotaur", value: "minotaur" },
{ label: "Orc", value: "orc" },
{ label: "Satyr", value: "satyr" },
{ label: "Shadar-Kai", value: "shadar-kai" },
{ label: "Shardmind", value: "shardmind" },
{ label: "Shifter", value: "shifter" },
{ label: "Simic Hybrid", value: "simic-hybrid" },
{ label: "Tabaxi", value: "tabaxi" },
{ label: "Tiefling", value: "tiefling" },
{ label: "Tortle", value: "tortle" },
{ label: "Triton", value: "triton" },
{ label: "Vedalken", value: "vedalken" },
{ label: "Verdan", value: "verdan" },
{ label: "Warforged", value: "warforged" },
{ label: "Wilden", value: "wilden" },
{ label: "Yuan-Ti", value: "yuan-ti" },
];
const race = ref(races[0].value);
const language = ref("english");
const gender = ref("male");
const fullNames = ref([]);
const showToast = ref(false);
const toastMessage = ref("");
function resetGenerator() {
modern.value = false;
race.value = races[0].value;
language.value = "english";
gender.value = "male";
fullNames.value = [];
showToast.value = false;
}
async function generate() {
fullNames.value = [];
isLoading.value = true;
try {
if (modern.value) {
// Use API for modern names
const url = `https://namegen.fernandovideira.com/v1/modern/${language.value}?gender=${gender.value}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success && data.names) {
fullNames.value = data.names;
} else {
console.error(
"Failed to fetch modern names:",
data.message || "Unknown error"
);
fullNames.value = ["Error generating names"];
}
} else {
// Use API for fantasy names
const url = `https://namegen.fernandovideira.com/v1/fantasy/${race.value}-names?gender=${gender.value}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success && data.names) {
fullNames.value = data.names;
} else {
console.error(
"Failed to fetch fantasy names:",
data.message || "Unknown error"
);
fullNames.value = ["Error generating names"];
}
}
} catch (error) {
console.error("Error fetching names:", error);
fullNames.value = [`Error: ${error.message}`];
} finally {
isLoading.value = false;
}
}
async function copyToClipboard(name) {
try {
await navigator.clipboard.writeText(name);
showToastMessage(`"${name}" copied to clipboard!`);
} catch (err) {
console.error("Failed to copy to clipboard:", err);
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = name;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
showToastMessage(`"${name}" copied to clipboard!`);
}
}
function showToastMessage(message) {
toastMessage.value = message;
showToast.value = true;
setTimeout(() => {
showToast.value = false;
}, 2000);
}
</script>
<style scoped>
.name-generator-card {
background: var(--color-card);
border: 1px solid var(--color-card-border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
padding: var(--spacing-md);
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
min-height: 400px;
position: relative;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-sm);
}
.card-header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--color-text);
}
.reset-btn {
background: transparent;
border: 1px solid var(--color-card-border);
border-radius: var(--radius-md);
padding: var(--spacing-xs);
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.reset-btn:hover {
background: var(--color-surface-hover);
color: var(--color-text);
border-color: var(--color-primary);
}
.controls {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.name-type-switch {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.switch-label {
font-size: 0.9rem;
color: var(--color-text-muted);
font-weight: 500;
}
.switch-container {
display: flex;
border: 1px solid var(--color-card-border);
border-radius: var(--radius-md);
overflow: hidden;
background: var(--color-background);
}
.switch-button {
flex: 1;
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--color-background);
border: none;
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
font-weight: 500;
}
.switch-button:hover {
background: var(--color-surface-hover);
color: var(--color-text);
}
.switch-button.active {
background: #3b82f6;
color: white;
font-weight: 600;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
}
.switch-button + .switch-button {
border-left: 1px solid var(--color-card-border);
}
label {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
font-size: 0.9rem;
color: var(--color-text-muted);
font-weight: 500;
}
select {
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--color-card-border);
border-radius: var(--radius-md);
background: var(--color-background);
color: var(--color-text);
font-size: 0.9rem;
}
select:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px var(--color-primary-alpha);
}
.button-generate {
padding: var(--spacing-sm) var(--spacing-md);
background: #3b82f6;
color: white;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-weight: 600;
transition: all 0.2s ease;
margin-top: var(--spacing-sm);
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
}
.button-generate:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
}
.button-generate:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.results {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
flex: 1;
overflow-y: auto;
max-height: 200px;
}
.name-item {
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--surface-secondary);
border: 1px solid var(--border);
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9rem;
}
.name-item:hover {
background: var(--surface-hover);
border-color: var(--border-hover);
transform: translateX(2px);
}
.copy-icon {
opacity: 0;
transition: opacity 0.2s ease;
color: var(--text-secondary);
}
.name-item:hover .copy-icon {
opacity: 1;
}
.toast {
position: absolute;
bottom: var(--spacing-sm);
left: 50%;
transform: translateX(-50%);
background: var(--success);
color: var(--success-text);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--border-radius);
font-size: 0.8rem;
font-weight: 500;
z-index: 10;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(10px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
</style>