From 362bdd5857178e35a1852c7b330681bd92c13d0b Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 16 Mar 2026 17:43:18 -0700 Subject: [PATCH] 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 --- modules/crowdsurf/mod.ts | 1 + .../components/folk-choices-dashboard.ts | 44 +++---------------- modules/rchoices/mod.ts | 31 ++++++++++--- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/modules/crowdsurf/mod.ts b/modules/crowdsurf/mod.ts index 826a570..89eb423 100644 --- a/modules/crowdsurf/mod.ts +++ b/modules/crowdsurf/mod.ts @@ -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, diff --git a/modules/rchoices/components/folk-choices-dashboard.ts b/modules/rchoices/components/folk-choices-dashboard.ts index 8d5ee0b..a14fa98 100644 --- a/modules/rchoices/components/folk-choices-dashboard.ts +++ b/modules/rchoices/components/folk-choices-dashboard.ts @@ -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 {
Choices DEMO - -
- -
- ${tabs.map((t) => ``).join("")} +
@@ -1104,16 +1082,6 @@ class FolkChoicesDashboard extends HTMLElement { private bindDemoEvents() { this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour()); - // Tab switching - this.shadow.querySelectorAll(".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(".spider-legend-item").forEach((el) => { diff --git a/modules/rchoices/mod.ts b/modules/rchoices/mod.ts index 4810885..5022206 100644 --- a/modules/rchoices/mod.ts +++ b/modules/rchoices/mod.ts @@ -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: ``, - scripts: ``, + body: ``, + scripts: ``, + styles: ``, + })); +}); + +// 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: ``, + scripts: ``, styles: ``, })); }); @@ -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" }, ], };