Important Fixes, New mechanics, and many more
Some checks failed
CI / validate (push) Failing after 9m21s
Some checks failed
CI / validate (push) Failing after 9m21s
This commit is contained in:
@@ -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>
|
||||
|
||||
52
apps/web/components/CoauthorConversation.vue
Normal file
52
apps/web/components/CoauthorConversation.vue
Normal 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>
|
||||
32
apps/web/components/CoauthorWorldPreview.vue
Normal file
32
apps/web/components/CoauthorWorldPreview.vue
Normal 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>
|
||||
Reference in New Issue
Block a user