Important Fixes, New mechanics, and many more
Some checks failed
CI / validate (push) Failing after 9m21s

This commit is contained in:
2026-08-15 17:37:57 +05:00
parent 1774496cf9
commit 438e5af0ad
56 changed files with 5090 additions and 119 deletions

View File

@@ -1,5 +1,18 @@
<script setup lang="ts">
defineProps<{ section?: string }>()
const { session, restore, signOut } = useDngAuth()
onMounted(() => void restore())
const initials = computed(() => {
const email = session.value?.user.email ?? ''
return email.slice(0, 2).toUpperCase() || 'G'
})
async function leave() {
if (session.value?.user.is_anonymous && !window.confirm('This guest account cannot be recovered after sign out. Leave anyway?')) return
await signOut()
await navigateTo('/')
}
</script>
<template>
@@ -9,7 +22,8 @@ defineProps<{ section?: string }>()
<div class="topbar-center"><span class="status-dot" /> PRIVATE ALPHA <b v-if="section">/ {{ section }}</b></div>
<div class="topbar-actions">
<NuxtLink to="/dashboard" class="icon-link" aria-label="Dashboard"></NuxtLink>
<div class="avatar">MV</div>
<button v-if="session" class="avatar" :title="session.user.is_anonymous ? 'Guest session — sign out' : `Sign out ${session.user.email ?? ''}`" @click="leave">{{ initials }}</button>
<div v-else class="avatar">D&G</div>
</div>
</header>
<main><slot /></main>
@@ -17,5 +31,5 @@ defineProps<{ section?: string }>()
</template>
<style scoped>
.shell{min-height:100vh}.topbar{position:sticky;top:0;z-index:20;height:76px;padding:0 clamp(18px,4vw,64px);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;border-bottom:1px solid var(--line);background:rgba(10,10,10,.9);backdrop-filter:blur(18px)}.topbar-center{font:500 10px/1 var(--mono);color:var(--muted);letter-spacing:.18em}.topbar-center b{color:var(--ink);font-weight:500}.status-dot{display:inline-block;width:6px;height:6px;margin-right:8px;border-radius:50%;background:var(--acid);box-shadow:0 0 12px var(--acid)}.topbar-actions{justify-self:end;display:flex;align-items:center;gap:14px}.icon-link{display:grid;place-items:center;width:36px;height:36px;border:1px solid var(--line);color:var(--muted);text-decoration:none}.avatar{display:grid;place-items:center;width:36px;height:36px;border-radius:50%;background:var(--acid);color:#0a0a0a;font:700 10px var(--mono)}@media(max-width:700px){.topbar{grid-template-columns:1fr auto}.topbar-center{display:none}}
.shell{min-height:100vh}.topbar{position:sticky;top:0;z-index:20;height:76px;padding:0 clamp(18px,4vw,64px);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;border-bottom:1px solid var(--line);background:rgba(10,10,10,.9);backdrop-filter:blur(18px)}.topbar-center{font:500 10px/1 var(--mono);color:var(--muted);letter-spacing:.18em}.topbar-center b{color:var(--ink);font-weight:500}.status-dot{display:inline-block;width:6px;height:6px;margin-right:8px;border-radius:50%;background:var(--acid);box-shadow:0 0 12px var(--acid)}.topbar-actions{justify-self:end;display:flex;align-items:center;gap:14px}.icon-link{display:grid;place-items:center;width:36px;height:36px;border:1px solid var(--line);color:var(--muted);text-decoration:none}.avatar{display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:50%;background:var(--acid);color:#0a0a0a;font:700 10px var(--mono)}@media(max-width:700px){.topbar{grid-template-columns:1fr auto}.topbar-center{display:none}}
</style>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
interface Message { id: string; role: 'coauthor' | 'player' | 'status'; body: string; label?: string }
interface Question { key: string; label: string; options: string[] }
const props = defineProps<{
messages: Message[]
question: Question | null
selectedAnswer?: string
busy?: boolean
}>()
const emit = defineEmits<{
answer: [key: string, value: string]
retry: []
}>()
const customAnswer = ref('')
function submitCustom() {
if (!props.question || !customAnswer.value.trim()) return
emit('answer', props.question.key, customAnswer.value.trim())
customAnswer.value = ''
}
</script>
<template>
<section class="conversation" aria-live="polite">
<ol class="messages">
<li v-for="message in messages" :key="message.id" :class="message.role">
<small>{{ message.label || (message.role === 'player' ? 'YOU' : message.role === 'status' ? 'SYSTEM' : 'COAUTHOR') }}</small>
<p>{{ message.body }}</p>
</li>
</ol>
<div v-if="question" class="question-card">
<p>{{ question.label }}</p>
<div class="options">
<button v-for="option in question.options" :key="option" type="button" :class="{ selected: selectedAnswer === option }" :disabled="busy" @click="emit('answer', question.key, option)">{{ option }}</button>
</div>
<form class="custom" @submit.prevent="submitCustom">
<input v-model="customAnswer" :disabled="busy" maxlength="160" placeholder="Or write your own answer…" :aria-label="`Custom answer: ${question.label}`">
<button :disabled="busy || !customAnswer.trim()">SEND </button>
</form>
</div>
<button v-if="messages.at(-1)?.role === 'status' && messages.at(-1)?.label === 'ERROR'" class="retry" type="button" @click="emit('retry')">TRY AGAIN</button>
</section>
</template>
<style scoped>
.conversation{max-width:780px}.messages{list-style:none;padding:0;margin:0}.messages li{max-width:680px;margin:22px 0;padding:5px 20px;border-left:2px solid var(--acid)}.messages li.player{margin-left:auto;border-left:0;border-right:2px solid #5f5f58;text-align:right}.messages li.status{border:1px solid var(--line);background:#10100f}.messages li.status[aria-invalid=true]{border-color:#7b4137}.messages small{font:500 8px var(--mono);letter-spacing:.15em;color:var(--acid)}.messages .player small{color:var(--ink)}.messages .status small{color:var(--muted)}.messages p{margin:9px 0 0;font-size:14px;line-height:1.65;color:#d0d0ca;white-space:pre-wrap}.question-card{margin:28px 0 0 20px;padding:24px;border:1px solid var(--line);background:#0d0d0c}.question-card>p{margin:0 0 18px;font:600 14px/1.5 var(--display)}.options{display:flex;gap:9px;flex-wrap:wrap}.options button,.retry{padding:12px 14px;border:1px solid var(--line);background:#10100f;color:var(--muted);font:500 9px var(--mono)}.options button:hover,.options button.selected{border-color:var(--acid);color:var(--acid)}button:disabled{opacity:.45;cursor:not-allowed}.custom{display:flex;margin-top:12px}.custom input{min-width:0;flex:1;padding:13px 14px;border:1px solid var(--line);background:#080808;color:var(--ink);font:12px var(--body)}.custom button{border:0;background:var(--acid);color:#080808;padding:0 17px;font:600 8px var(--mono)}.retry{margin:18px 0 0 20px;color:#ff9d85;border-color:#7b4137}@media(max-width:600px){.messages li.player{margin-left:28px}.question-card{margin-left:0}.custom{flex-direction:column}.custom button{min-height:42px}}
</style>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { WorldStarter } from '@dng/shared'
const draft = defineModel<WorldStarter>({ required: true })
defineEmits<{ revise: []; confirm: [] }>()
</script>
<template>
<section class="preview">
<header>
<div><p class="kicker">GENERATED STARTING KIT</p><input v-model="draft.title" aria-label="World title"></div>
<button class="revise" type="button" @click="$emit('revise')"> REVISE ANSWERS</button>
</header>
<p class="edit-note">Everything below is editable before launch.</p>
<div class="grid">
<section class="wide"><label>PREMISE</label><textarea v-model="draft.premise" rows="5" /></section>
<section><label>GENRE</label><input v-model="draft.genre"></section>
<section><label>TONE</label><input v-model="draft.tone"></section>
<section><label>STARTING LOCATION</label><input v-model="draft.startingLocation.name"><textarea v-model="draft.startingLocation.summary" rows="3" /></section>
<section><label>OPENING HOOK</label><textarea v-model="draft.hook" rows="5" /></section>
<section class="wide"><label>KEY PEOPLE</label><div class="entities"><article v-for="npc in draft.npcs" :key="npc.id"><small>NPC</small><input v-model="npc.name"><textarea v-model="npc.summary" rows="3" /></article></div></section>
<section class="wide"><label>FACTIONS</label><div class="entities two"><article v-for="faction in draft.factions" :key="faction.id"><small>FACTION</small><input v-model="faction.name"><textarea v-model="faction.summary" rows="3" /></article></div></section>
<section><label>OPENING SCENE</label><textarea v-model="draft.openingScene" rows="6" /></section>
<section><label>CONTENT BOUNDARIES</label><div v-for="(_, index) in draft.contentBoundaries" :key="index" class="boundary"><input v-model="draft.contentBoundaries[index]"><button type="button" aria-label="Remove boundary" @click="draft.contentBoundaries.splice(index, 1)">×</button></div><button class="add" type="button" @click="draft.contentBoundaries.push('')"> ADD BOUNDARY</button></section>
</div>
<footer><p><b>PRIVATE WORLD</b><span>Only invited campaign members can see it.</span></p><button type="button" @click="$emit('confirm')">CONFIRM & ENTER WORLD </button></footer>
</section>
</template>
<style scoped>
.preview{max-width:1300px;width:100%;padding:clamp(40px,5vw,76px)}header{display:flex;justify-content:space-between;align-items:end;gap:20px}.kicker,label{font:600 8px var(--mono);letter-spacing:.15em;color:var(--acid)}header>div{flex:1}header input{border:0;border-bottom:1px solid var(--line);font:600 clamp(30px,4vw,54px) var(--display);letter-spacing:-.05em;padding:12px 0;background:transparent}.revise,.add{background:transparent;color:var(--muted);border:1px solid var(--line);padding:12px;font:500 8px var(--mono)}.edit-note{color:var(--muted);font-size:11px}.grid{display:grid;grid-template-columns:1fr 1fr;border-top:1px solid var(--line);border-left:1px solid var(--line);margin-top:28px}.grid>section{padding:26px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}.wide{grid-column:1/-1}input,textarea{box-sizing:border-box;width:100%;margin-top:12px;padding:13px;resize:vertical;background:#10100f;border:1px solid var(--line);color:var(--ink);font:12px/1.55 var(--body)}.entities{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:18px}.entities.two{grid-template-columns:repeat(2,1fr)}.entities article{padding:16px;border:1px solid var(--line);background:#0c0c0b}.entities small{font:500 7px var(--mono);color:var(--acid)}.boundary{display:flex}.boundary input{margin-top:8px}.boundary button{margin-top:8px;width:42px;border:1px solid var(--line);background:#151513;color:var(--muted)}.add{margin-top:12px}footer{position:sticky;bottom:0;display:flex;justify-content:space-between;align-items:center;padding:18px 22px;background:#11110f;border:1px solid var(--line);margin-top:24px}footer p{margin:0;display:flex;flex-direction:column;font:600 8px var(--mono);color:var(--acid)}footer p span{margin-top:5px;color:var(--muted);font-weight:400}footer>button{min-height:48px;border:0;background:var(--acid);color:#080808;padding:0 24px;font:600 9px var(--mono)}@media(max-width:760px){.preview{padding:36px 18px}header{align-items:stretch;flex-direction:column}.grid{grid-template-columns:1fr}.wide{grid-column:auto}.entities,.entities.two{grid-template-columns:1fr}footer{align-items:stretch;flex-direction:column;gap:14px}}
</style>