Compare commits

...

2 Commits

Author SHA1 Message Date
57f5fc7700
Merge pull request #50 from mrteye/feat-sessions-drawer-machine-11417224878052948069
feat(ui): implement 2-state workshop drawer machine and refine session page hierarchy

Reorganized the Sessions page hierarchy to place the Event Cockpit below the main header. Refactored the Workshop Drawer to utilize a strict 2-state container machine for clean transitions between the creation form and the success handoff modal. Implemented UI accessibility enhancements and fixed mobile text wrapping constraints.
2026-08-26 17:14:32 -07:00
google-labs-jules[bot]
d057e080d3 feat(ui): implement 2-state workshop drawer machine and refine session page hierarchy
- Moved `EventCockpitDeck` below main header in `SessionsPage.tsx`
- Refactored `WorkshopDrawer.tsx` to strictly use a 2-state display toggle (`#eventCreateState` and `#eventHandoffState`)
- Added `aria-label`s to copy buttons for accessibility
- Configured `#status-banner` with `role="status"` and `aria-live="polite"`
- Fixed mobile title text wrapping on `#createdEventTitle`
- Fixed script emoji injection logic to prevent double emojis
- Added JS reset logic in `closeDelegateDrawer` to restore drawer states and clear form data

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-27 00:12:50 +00:00
4 changed files with 218 additions and 190 deletions

View File

@ -28,11 +28,11 @@ export const SessionsPage = ({
> >
<div <div
id="status-banner" id="status-banner"
role="status"
aria-live="polite"
style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;" style="display: none; margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); font-size: 0.9rem;"
/> />
<EventCockpitDeck eventPasses={eventPasses} />
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
<div> <div>
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);"> <h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
@ -57,6 +57,8 @@ export const SessionsPage = ({
</div> </div>
</div> </div>
<EventCockpitDeck eventPasses={eventPasses} />
{/* Delegate Session Drawer */} {/* Delegate Session Drawer */}
<div <div
id="delegateDrawer" id="delegateDrawer"
@ -66,7 +68,10 @@ export const SessionsPage = ({
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.5rem;">
<div style="width: 100%;"> <div style="width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0 0 0.25rem 0; color: var(--text-primary);"> <h3
id="delegateDrawerTitle"
style="margin: 0 0 0.25rem 0; color: var(--text-primary);"
>
Create New Pass / Session Create New Pass / Session
</h3> </h3>
<button <button

View File

@ -22,6 +22,15 @@ export const SessionsScript = () => {
function closeDelegateDrawer() { function closeDelegateDrawer() {
document.getElementById('delegateDrawer').style.display = 'none'; document.getElementById('delegateDrawer').style.display = 'none';
// Reset WorkshopDrawer 2-State Machine
const eventCreateState = document.getElementById('eventCreateState');
if (eventCreateState) eventCreateState.style.display = 'block';
const eventHandoffState = document.getElementById('eventHandoffState');
if (eventHandoffState) eventHandoffState.style.display = 'none';
const eventForm = document.getElementById('eventForm');
if (eventForm) eventForm.reset();
const drawerTitle = document.getElementById('delegateDrawerTitle');
if (drawerTitle) drawerTitle.textContent = 'Create New Pass / Session';
} }
function switchDelegateTab(tabId) { function switchDelegateTab(tabId) {
@ -150,11 +159,15 @@ export const SessionsScript = () => {
const data = await res.json(); const data = await res.json();
if (res.ok && data.success) { if (res.ok && data.success) {
const ev = data.event; const ev = data.event;
document.getElementById('createdEventTitle').textContent = '🎟️ ' + ev.name + ' Live!'; document.getElementById('createdEventTitle').textContent = ev.name + ' Live!';
document.getElementById('eventHandoffPin').textContent = ev.pin_code; document.getElementById('eventHandoffPin').textContent = ev.pin_code;
document.getElementById('eventHandoffLink').textContent = window.location.origin + '/e/' + ev.slug; document.getElementById('eventHandoffLink').textContent = window.location.origin + '/e/' + ev.slug;
document.getElementById('eventHandoffCli').textContent = 'curl -sSL ' + window.location.origin + '/join/' + ev.slug + '?format=env | source /dev/stdin'; document.getElementById('eventHandoffCli').textContent = 'curl -sSL ' + window.location.origin + '/join/' + ev.slug + '?format=env | source /dev/stdin';
document.getElementById('eventHandoffModal').style.display = 'block';
document.getElementById('eventCreateState').style.display = 'none';
document.getElementById('eventHandoffState').style.display = 'block';
document.getElementById('delegateDrawerTitle').textContent = 'Event Pass Active';
showNotice('Workshop pass created: ' + ev.name, false); showNotice('Workshop pass created: ' + ev.name, false);
} else { } else {
showNotice(data.error || 'Failed to create event pass', true); showNotice(data.error || 'Failed to create event pass', true);
@ -177,7 +190,7 @@ export const SessionsScript = () => {
} }
function closeEventHandoffModal() { function closeEventHandoffModal() {
document.getElementById('eventHandoffModal').style.display = 'none'; closeDelegateDrawer();
window.location.reload(); window.location.reload();
} }

View File

@ -1,217 +1,224 @@
export const WorkshopDrawer = ({ apps = [] }: { apps?: any[] }) => { export const WorkshopDrawer = ({ apps = [] }: { apps?: any[] }) => {
return ( return (
<div id="tabWorkshopPass" style="display: none;"> <div id="tabWorkshopPass" style="display: none;">
<p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;"> <div id="eventCreateState" style="display: block;">
Generate a shared event pass with a universal 6-digit PIN, vanity link <p style="color: var(--text-secondary); font-size: 0.9rem; margin: 0 0 1rem 0;">
(<code style="font-size: 0.8rem;">/e/slug</code>), and CLI 1-liner for Generate a shared event pass with a universal 6-digit PIN, vanity link
workshops, hackathons, and multi-user kiosks. (<code style="font-size: 0.8rem;">/e/slug</code>), and CLI 1-liner for
</p> workshops, hackathons, and multi-user kiosks.
</p>
<form <form
id="eventForm" id="eventForm"
onsubmit="handleCreateEvent(event)" onsubmit="handleCreateEvent(event)"
style="margin-top: 1rem;" style="margin-top: 1rem;"
> >
{/* Event Name */} {/* Event Name */}
<div style="margin-bottom: 1.25rem;"> <div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Workshop / Event Name *
</label>
<input
type="text"
id="eventName"
placeholder="e.g. Deno & SPIRE Zero-Trust Workshop, Elite Fleet Live Demo"
required
style="width: 100%;"
/>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
{/* Target App */}
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);"> <label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Target App Destination Workshop / Event Name *
</label> </label>
<select
id="eventAppId"
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
>
<option value="">Universal Sandbox (Default)</option>
{apps.map((app) => (
<option value={app.id} key={app.id}>
{app.name} ({app.domain || "Internal"})
</option>
))}
</select>
</div>
{/* Guest Role */}
<div>
<label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Attendee Access Role
</label>
<select
id="eventRole"
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
>
<option value="viewer">Viewer (Read-Only Demo)</option>
<option value="operator">Operator (Workshop Actions)</option>
<option value="user">Standard User</option>
</select>
</div>
</div>
{/* Seat Capacity Presets */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
Seat Capacity Limit
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 25)"
>
25 Seats
</button>
<button
type="button"
class="pill-btn seat-pill active"
onclick="selectSeats(this, 50)"
>
50 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 100)"
>
100 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 0)"
>
Unlimited
</button>
<input <input
type="number" type="text"
id="eventMaxSeats" id="eventName"
value="50" placeholder="e.g. Deno & SPIRE Zero-Trust Workshop, Elite Fleet Live Demo"
min="0" required
max="10000" style="width: 100%;"
style="width: 100px; padding: 0.35rem 0.5rem; text-align: center;"
title="Max seats (0 = unlimited)"
/> />
</div> </div>
</div>
{/* Duration Presets */} <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.25rem;">
<div style="margin-bottom: 1.25rem;"> {/* Target App */}
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
Event Duration & Expiration
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 1)"
>
1 Hour
</button>
<button
type="button"
class="pill-btn event-lifespan-pill active"
onclick="selectEventLifespan(this, 3)"
>
🛠 3 Hours (Standard Workshop)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 12)"
>
📅 12 Hours (All-Day Hackathon)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 24)"
>
🗓 24 Hours
</button>
</div>
<input type="hidden" id="eventLifespanHours" value="3" />
</div>
{/* Custom Codes Accordion (Optional) */}
<details style="margin-bottom: 1.5rem; background: var(--surface-muted); padding: 0.75rem 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
<summary style="font-size: 0.875rem; font-weight: 600; color: var(--text-primary); cursor: pointer;">
Custom Vanity Slug & Custom PIN Code (Optional)
</summary>
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
<div> <div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;"> <label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Vanity Slug (e.g. /e/deno-lab) Target App Destination
</label> </label>
<input <select
type="text" id="eventAppId"
id="eventSlug" style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
placeholder="auto-generated if blank" >
style="width: 100%; font-size: 0.85rem;" <option value="">Universal Sandbox (Default)</option>
/> {apps.map((app) => (
<option value={app.id} key={app.id}>
{app.name} ({app.domain || "Internal"})
</option>
))}
</select>
</div> </div>
{/* Guest Role */}
<div> <div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;"> <label style="display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.875rem; color: var(--text-secondary);">
Custom 6-Digit PIN (e.g. 749-123) Attendee Access Role
</label> </label>
<select
id="eventRole"
style="width: 100%; padding: 0.5rem 0.75rem; background: var(--surface-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); color: var(--text-primary); font-size: 0.9rem;"
>
<option value="viewer">Viewer (Read-Only Demo)</option>
<option value="operator">Operator (Workshop Actions)</option>
<option value="user">Standard User</option>
</select>
</div>
</div>
{/* Seat Capacity Presets */}
<div style="margin-bottom: 1.25rem;">
<label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
Seat Capacity Limit
</label>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;">
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 25)"
>
25 Seats
</button>
<button
type="button"
class="pill-btn seat-pill active"
onclick="selectSeats(this, 50)"
>
50 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 100)"
>
100 Seats
</button>
<button
type="button"
class="pill-btn seat-pill"
onclick="selectSeats(this, 0)"
>
Unlimited
</button>
<input <input
type="text" type="number"
id="eventPinCode" id="eventMaxSeats"
placeholder="auto-generated if blank" value="50"
style="width: 100%; font-size: 0.85rem;" min="0"
max="10000"
style="width: 100px; padding: 0.35rem 0.5rem; text-align: center;"
title="Max seats (0 = unlimited)"
/> />
</div> </div>
</div> </div>
</details>
<div style="display: flex; gap: 0.75rem;"> {/* Duration Presets */}
<button <div style="margin-bottom: 1.25rem;">
type="submit" <label style="display: block; font-weight: 600; margin-bottom: 0.45rem; font-size: 0.875rem; color: var(--text-secondary);">
id="submitEventBtn" Event Duration & Expiration
class="btn-primary" </label>
style="min-height: 42px; background: var(--success); border-color: var(--success);" <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
> <button
🎟 Launch Workshop Pass type="button"
</button> class="pill-btn event-lifespan-pill"
<button onclick="selectEventLifespan(this, 1)"
type="button" >
class="btn-outline" 1 Hour
onclick="closeDelegateDrawer()" </button>
style="min-height: 42px;" <button
> type="button"
Cancel class="pill-btn event-lifespan-pill active"
</button> onclick="selectEventLifespan(this, 3)"
</div> >
</form> 🛠 3 Hours (Standard Workshop)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 12)"
>
📅 12 Hours (All-Day Hackathon)
</button>
<button
type="button"
class="pill-btn event-lifespan-pill"
onclick="selectEventLifespan(this, 24)"
>
🗓 24 Hours
</button>
</div>
<input type="hidden" id="eventLifespanHours" value="3" />
</div>
{/* Event Launch Success Modal / Card */} {/* Custom Codes Accordion (Optional) */}
<details style="margin-bottom: 1.5rem; background: var(--surface-muted); padding: 0.75rem 1rem; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
<summary style="font-size: 0.875rem; font-weight: 600; color: var(--text-primary); cursor: pointer;">
Custom Vanity Slug & Custom PIN Code (Optional)
</summary>
<div style="margin-top: 0.75rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
<div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
Vanity Slug (e.g. /e/deno-lab)
</label>
<input
type="text"
id="eventSlug"
placeholder="auto-generated if blank"
style="width: 100%; font-size: 0.85rem;"
/>
</div>
<div>
<label style="display: block; font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 0.25rem;">
Custom 6-Digit PIN (e.g. 749-123)
</label>
<input
type="text"
id="eventPinCode"
placeholder="auto-generated if blank"
style="width: 100%; font-size: 0.85rem;"
/>
</div>
</div>
</details>
<div style="display: flex; gap: 0.75rem;">
<button
type="submit"
id="submitEventBtn"
class="btn-primary"
style="min-height: 42px; background: var(--success); border-color: var(--success);"
>
🎟 Launch Workshop Pass
</button>
<button
type="button"
class="btn-outline"
onclick="closeDelegateDrawer()"
style="min-height: 42px;"
>
Cancel
</button>
</div>
</form>
</div>
{/* Event Launch Success State */}
<div <div
id="eventHandoffModal" id="eventHandoffState"
style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-card); border: 2px solid var(--success); border-radius: var(--radius-md); box-shadow: var(--shadow-md);" style="display: none; margin-top: 1.5rem; padding: 1.25rem; background: var(--surface-card); border: 2px solid var(--success); border-radius: var(--radius-md); box-shadow: var(--shadow-md);"
> >
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.5rem;"> <div style="display: flex; align-items: center; gap: 0.5rem; flex: 1; min-width: 0;">
<span style="font-size: 1.3rem;">🎟</span> <span style="font-size: 1.3rem; flex-shrink: 0;">🎟</span>
<strong <strong
id="createdEventTitle" id="createdEventTitle"
style="color: var(--text-primary); font-size: 1.1rem;" style="color: var(--text-primary); font-size: 1.1rem; overflow-wrap: break-word; word-break: break-word; max-width: 100%; display: block;"
> >
Workshop Pass Live! Workshop Pass Live!
</strong> </strong>
</div> </div>
<span class="badge badge-success">Active Now</span> <span
class="badge badge-success"
style="flex-shrink: 0; margin-left: 0.5rem;"
>
Active Now
</span>
</div> </div>
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0 0 1rem 0;"> <p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0 0 1rem 0;">
@ -235,6 +242,7 @@ export const WorkshopDrawer = ({ apps = [] }: { apps?: any[] }) => {
<button <button
type="button" type="button"
class="btn-primary" class="btn-primary"
aria-label="Copy Universal PIN"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;" style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyEventHandoff('pin')" onclick="copyEventHandoff('pin')"
> >
@ -257,6 +265,7 @@ export const WorkshopDrawer = ({ apps = [] }: { apps?: any[] }) => {
<button <button
type="button" type="button"
class="btn-outline" class="btn-outline"
aria-label="Copy Direct Link"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;" style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyEventHandoff('link')" onclick="copyEventHandoff('link')"
> >
@ -279,6 +288,7 @@ export const WorkshopDrawer = ({ apps = [] }: { apps?: any[] }) => {
<button <button
type="button" type="button"
class="btn-outline" class="btn-outline"
aria-label="Copy Terminal curl command"
style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;" style="min-height: 36px; padding: 0 0.85rem; font-size: 0.8rem;"
onclick="copyEventHandoff('cli')" onclick="copyEventHandoff('cli')"
> >
@ -294,7 +304,7 @@ export const WorkshopDrawer = ({ apps = [] }: { apps?: any[] }) => {
style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;" style="width: 100%; min-height: 38px; justify-content: center; font-size: 0.85rem;"
onclick="closeEventHandoffModal()" onclick="closeEventHandoffModal()"
> >
Done & Refresh Cockpit Dismiss
</button> </button>
</div> </div>
</div> </div>