fix(rchoices): move CrowdSurf under rChoices sub-nav, fix header overlap

- Hide CrowdSurf from app switcher (hidden: true) since it's now a
  sub-tab of rChoices
- Replace dead outputPaths (Polls/Results with no routes) with actual
  tabs: Spider Chart, Ranking, Voting, CrowdSurf
- Add /:tab route handler so sub-nav pills link to working URLs
- Component reads tab attribute for initial tab selection
- Remove internal .demo-tabs (shell sub-nav replaces them)
- Bump JS cache version to v=6

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-16 17:43:18 -07:00
parent e0d976ac92
commit 362bdd5857
3 changed files with 33 additions and 43 deletions

View File

@ -131,6 +131,7 @@ export const crowdsurfModule: RSpaceModule = {
description: "Swipe-based community activity coordination",
scoping: { defaultScope: 'space', userConfigurable: false },
routes,
hidden: true, // CrowdSurf is now a sub-tab of rChoices
standaloneDomain: "crowdsurf.online",
landingPage: renderLanding,
seedTemplate: seedTemplateCrowdSurf,

View File

@ -70,15 +70,17 @@ class FolkChoicesDashboard extends HTMLElement {
// Guided tour
private _tour!: TourEngine;
private static readonly TOUR_STEPS = [
{ target: '[data-tab="spider"]', title: "Spider Charts", message: "Compare multiple criteria on a radar chart. Each participant's scores overlay in real time.", advanceOnClick: true },
{ target: '[data-tab="ranking"]', title: "Rankings", message: "Drag items to rank them. Rankings aggregate across all participants for a collective order.", advanceOnClick: true },
{ target: '[data-tab="voting"]', title: "Voting", message: "Cast your vote and watch results update live with animated bars and totals.", advanceOnClick: true },
{ target: '.demo-content', title: "rChoices", message: "Explore spider charts, rankings, live voting, and CrowdSurf swipe cards. Use the sub-nav above to switch between modes.", advanceOnClick: true },
];
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
this.space = this.getAttribute("space") || "demo";
const tabAttr = this.getAttribute("tab") as "spider" | "ranking" | "voting" | "crowdsurf" | null;
if (tabAttr && ["spider", "ranking", "voting", "crowdsurf"].includes(tabAttr)) {
this.demoTab = tabAttr;
}
this._tour = new TourEngine(
this.shadow,
FolkChoicesDashboard.TOUR_STEPS,
@ -527,13 +529,6 @@ class FolkChoicesDashboard extends HTMLElement {
}
private renderDemo() {
const tabs: { key: "spider" | "ranking" | "voting" | "crowdsurf"; label: string; icon: string }[] = [
{ key: "spider", label: "Spider Chart", icon: "🕸" },
{ key: "ranking", label: "Ranking", icon: "📊" },
{ key: "voting", label: "Live Voting", icon: "☑" },
{ key: "crowdsurf", label: "CrowdSurf", icon: "🏄" },
];
let content = "";
if (this.demoTab === "spider") content = this.renderSpider();
else if (this.demoTab === "ranking") content = this.renderRanking();
@ -548,15 +543,6 @@ class FolkChoicesDashboard extends HTMLElement {
.rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); }
.demo-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 999px; background: var(--rs-primary); color: #fff; font-weight: 500; }
/* Tabs */
.demo-tabs { display: flex; gap: 4px; margin-bottom: 1.5rem; margin-top: 4px; background: var(--rs-bg-page); border-radius: 10px; padding: 4px; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.demo-tab { flex: 1 1 0; min-width: 0; text-align: center; padding: 0.5rem 0.5rem; border-radius: 8px; border: none; background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 0.8rem; font-family: inherit; transition: all 0.15s; white-space: nowrap; }
.demo-tab:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface); }
.demo-tab.active { background: var(--rs-bg-surface); color: var(--rs-text-primary); box-shadow: var(--rs-shadow-sm); }
.demo-tab-icon { margin-right: 4px; }
.demo-tab-label { }
@media (max-width: 480px) { .demo-tab-label { display: none; } .demo-tab-icon { margin-right: 0; } .demo-tab { flex: 0 0 auto; padding: 0.5rem 0.75rem; } }
/* Spider chart */
.spider-wrap { display: flex; flex-direction: column; align-items: center; }
.spider-svg { width: 100%; max-width: 420px; }
@ -628,10 +614,6 @@ class FolkChoicesDashboard extends HTMLElement {
:host { padding: 1rem; }
.rapp-nav { gap: 4px; }
.create-btn { padding: 0.375rem 0.75rem; font-size: 0.8125rem; }
.demo-tabs { gap: 2px; padding: 3px; }
.demo-tab { padding: 0.5rem; font-size: 0.8125rem; }
.demo-tab-label { display: none; }
.demo-tab-icon { margin-right: 0; font-size: 1.1rem; }
.rank-item { padding: 0.625rem 0.75rem; gap: 8px; }
.rank-name { font-size: 0.875rem; }
.vote-option { padding: 0.625rem 0.75rem; }
@ -645,11 +627,7 @@ class FolkChoicesDashboard extends HTMLElement {
<div class="rapp-nav">
<span class="rapp-nav__title">Choices</span>
<span class="demo-badge">DEMO</span>
<button class="demo-tab" id="btn-tour" style="margin-left:auto;font-size:0.78rem">Tour</button>
</div>
<div class="demo-tabs">
${tabs.map((t) => `<button class="demo-tab${this.demoTab === t.key ? " active" : ""}" data-tab="${t.key}"><span class="demo-tab-icon">${t.icon}</span><span class="demo-tab-label">${this.esc(t.label)}</span></button>`).join("")}
<button style="margin-left:auto;padding:4px 10px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:0.78rem;font-family:inherit;" id="btn-tour">Tour</button>
</div>
<div class="demo-content">
@ -1104,16 +1082,6 @@ class FolkChoicesDashboard extends HTMLElement {
private bindDemoEvents() {
this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());
// Tab switching
this.shadow.querySelectorAll<HTMLButtonElement>(".demo-tab").forEach((btn) => {
btn.addEventListener("click", () => {
const tab = btn.dataset.tab as "spider" | "ranking" | "voting" | "crowdsurf";
if (tab && tab !== this.demoTab) {
this.demoTab = tab;
this.renderDemo();
}
});
});
// Spider legend hover
this.shadow.querySelectorAll<HTMLElement>(".spider-legend-item").forEach((el) => {

View File

@ -46,7 +46,7 @@ routes.get("/api/choices", async (c) => {
return c.json({ choices, total: choices.length });
});
// GET / — choices page
// GET / — choices page (default tab: spider)
routes.get("/", (c) => {
const spaceSlug = c.req.param("space") || "demo";
return c.html(renderShell({
@ -55,8 +55,27 @@ routes.get("/", (c) => {
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
body: `<folk-choices-dashboard space="${spaceSlug}"></folk-choices-dashboard>`,
scripts: `<script type="module" src="/modules/rchoices/folk-choices-dashboard.js?v=5"></script>`,
body: `<folk-choices-dashboard space="${spaceSlug}" tab="spider"></folk-choices-dashboard>`,
scripts: `<script type="module" src="/modules/rchoices/folk-choices-dashboard.js?v=6"></script>`,
styles: `<link rel="stylesheet" href="/modules/rchoices/choices.css">`,
}));
});
// GET /:tab — choices page with active sub-tab
routes.get("/:tab", (c) => {
const spaceSlug = c.req.param("space") || "demo";
const tab = c.req.param("tab");
const validTabs = ["spider", "ranking", "voting", "crowdsurf"];
if (!validTabs.includes(tab)) return c.notFound();
const tabLabel = tab === "crowdsurf" ? "CrowdSurf" : tab.charAt(0).toUpperCase() + tab.slice(1);
return c.html(renderShell({
title: `${spaceSlug}${tabLabel} | rChoices`,
moduleId: "rchoices",
spaceSlug,
modules: getModuleInfoList(),
theme: "dark",
body: `<folk-choices-dashboard space="${spaceSlug}" tab="${tab}"></folk-choices-dashboard>`,
scripts: `<script type="module" src="/modules/rchoices/folk-choices-dashboard.js?v=6"></script>`,
styles: `<link rel="stylesheet" href="/modules/rchoices/choices.css">`,
}));
});
@ -139,7 +158,9 @@ export const choicesModule: RSpaceModule = {
],
acceptsFeeds: ["data", "governance"],
outputPaths: [
{ path: "polls", name: "Polls", icon: "☑️", description: "Active and closed polls" },
{ path: "results", name: "Results", icon: "📊", description: "Poll and scoring results" },
{ path: "spider", name: "Spider Chart", icon: "🕸", description: "Multi-criteria radar charts" },
{ path: "ranking", name: "Ranking", icon: "📊", description: "Drag-and-drop rankings" },
{ path: "voting", name: "Voting", icon: "☑", description: "Live polls and voting" },
{ path: "crowdsurf", name: "CrowdSurf", icon: "🏄", description: "Swipe-based option surfacing" },
],
};