From e5edf0d5db682162691a71304cc093e09e9de75e Mon Sep 17 00:00:00 2001
From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com>
Date: Thu, 7 May 2026 22:28:35 -1000
Subject: [PATCH] =?UTF-8?q?Release=20v0.6.0=20=E2=80=94=20UI=20overhaul=20?=
=?UTF-8?q?+=20bookmark=20fix?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bug fixes
- Fix create_bookmark: relationships were silently stripped by Upjack's
base-field filter, so bookmarks had no link to a session and the UI
fell back to the literal string "Bookmarked session". New
bookmark_session(session_id) tool wraps create_entity with the
relationship set server-side.
- Fix host theme tokens not applying: synapse SDK 0.3.0 didn't inject
hostContext.styles.variables in the createSynapse path. Bump to 0.8.0.
UI overhaul (iframe app — ui://mcp-dev-summit/main)
- New self-contained palette in ui/src/styles.ts driven by data-theme,
using the host's --color-text-accent as the brand override and
computing the rest in CSS so the widget works with or without host
tokens.
- Typography hierarchy with real distance (11/12.5/14/18 px), mono
font for times, line-height tightening.
- Breaks demoted to dividers; keynotes get a 3px accent bar; bookmarked
rows get a gold bar.
- Bookmark control rebuilt as a 28px pill with + / ✓ icons (was a
unicode star with no affordance).
- Centered modal with sticky head + sticky action bar, animated entry,
Escape-to-dismiss.
- Empty states with iconography and helpful copy; bookmarks empty state
shows the actual + button inline.
- Search debounce 600ms → 250ms; suggestion chips; skeleton loading
rows.
- Day picker: segmented buttons with day-of-week eyebrow + date,
active state uses accent-soft (light) instead of harsh black fill.
UI overhaul (chat-embedded widgets — server.py)
- Single shared _WIDGET_CSS palette mirroring the iframe app, switched
via data-theme from the connect() theme + theme-changed event.
- Speaker widget: rule-separated rows, name + LinkedIn with proper
flex gap, subtle gray topic chips (not high-saturation pills),
mono session times.
- Session widget: keynote rows get the accent bar, mono time format,
badge + room + time meta line.
- Schedule widget: mono time slot headers, keynote accent bar,
unified meta row.
- Speaker card: same row pattern as the speaker widget.
Other
- Synapse provider name reads version from manifest (0.6.0).
- All 18 Python tests pass; UI type-checks and builds clean
(343 KB inlined / 100 KB gzipped).
---
manifest.json | 2 +-
pyproject.toml | 2 +-
server.json | 2 +-
src/mcp_dev_summit/__init__.py | 2 +-
src/mcp_dev_summit/server.py | 497 ++++++++------
ui/dist/index.html | 980 ++++++++++++++++++++-------
ui/package-lock.json | 8 +-
ui/package.json | 2 +-
ui/src/App.tsx | 1152 +++++++++++++++-----------------
ui/src/styles.ts | 780 +++++++++++++++++++++
uv.lock | 2 +-
11 files changed, 2360 insertions(+), 1069 deletions(-)
create mode 100644 ui/src/styles.ts
diff --git a/manifest.json b/manifest.json
index 6453be5..2005ba2 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": "0.4",
"name": "@nimblebraininc/synapse-mcp-dev-summit",
- "version": "0.5.0",
+ "version": "0.6.0",
"description": "Conference companion for MCP Dev Summit NA 2026 — search sessions, build a personal schedule, capture notes, and get AI-powered recommendations",
"author": {
"name": "NimbleBrain Inc",
diff --git a/pyproject.toml b/pyproject.toml
index 2691bae..a1239f2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "mcp-dev-summit"
-version = "0.5.0"
+version = "0.6.0"
description = "MCP Dev Summit Server"
readme = "README.md"
license = {text = "MIT"}
diff --git a/server.json b/server.json
index 72cbda6..55ab1d7 100644
--- a/server.json
+++ b/server.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-server.schema.json",
"name": "ai.nimblebrain/mcp-dev-summit",
"description": "Conference companion for MCP Dev Summit NA 2026 — search sessions, build a personal schedule, capture notes, and get AI-powered recommendations",
- "version": "0.5.0",
+ "version": "0.6.0",
"title": "MCP Dev Summit Companion",
"repository": {
"url": "https://github.com/NimbleBrainInc/mcp-dev-summit",
diff --git a/src/mcp_dev_summit/__init__.py b/src/mcp_dev_summit/__init__.py
index 3d18726..906d362 100644
--- a/src/mcp_dev_summit/__init__.py
+++ b/src/mcp_dev_summit/__init__.py
@@ -1 +1 @@
-__version__ = "0.5.0"
+__version__ = "0.6.0"
diff --git a/src/mcp_dev_summit/server.py b/src/mcp_dev_summit/server.py
index 74cfa04..bf0c38b 100644
--- a/src/mcp_dev_summit/server.py
+++ b/src/mcp_dev_summit/server.py
@@ -47,7 +47,9 @@
"- get_day_schedule: full schedule for a day\n"
"- whats_on: what's happening now/next\n"
"- browse_sponsors: sponsor directory by tier\n"
- "- create_bookmark / list_bookmarks: manage personal schedule\n"
+ "- bookmark_session(session_id): add a session to the user's personal schedule "
+ "(use this — it links the bookmark to the session). list_bookmarks / delete_bookmark "
+ "for read/remove.\n"
"- create_note / list_notes: capture session notes\n"
"- create_connection: track people you meet"
)
@@ -69,6 +71,7 @@
"create_bookmark",
"list_bookmarks",
"delete_bookmark",
+ "bookmark_session",
"create_note",
"list_notes",
"update_note",
@@ -159,47 +162,20 @@ def speaker_widget_ui() -> str:
"""Speaker card widget using Synapse.connect() for MCP Apps protocol."""
synapse_js = _SYNAPSE_JS
return (
- """
Speaker not found.
', "speaker-card")
- # Get their sessions via reverse index
try:
sessions = upjack_app.query_by_relationship("session", "presented_by", speaker_id)
except Exception:
sessions = []
photo = sp.get("photo_url", "")
- name = sp.get("name", "Unknown")
- role = sp.get("role", "")
- company = sp.get("company", "")
- bio = sp.get("bio", "")
+ name = html_mod.escape(sp.get("name", "Unknown"))
+ role = html_mod.escape(sp.get("role", ""))
+ company = html_mod.escape(sp.get("company", ""))
+ bio = html_mod.escape(sp.get("bio", ""))
topics = sp.get("topics", [])
linkedin = sp.get("linkedin_url", "")
- topics_html = "".join(f''
- f"{s.get('day', '')[-5:]} {s.get('start_time', '')} — {s.get('title', '')}
"
- for s in sessions
+ avatar = (
+ f''
- f'
{stype} '
+ f'
'
f"{title_html}"
- f'
{day} {start}-{end} {room}
'
- f"{f'
{speakers}
' if speakers else ''}"
- f"{f'
{desc}
' if desc else ''}"
+ f'
{"".join(meta_parts)}
'
+ f"{f'
{speakers}
' if speakers else ''}"
+ f"{f'
{desc}…
' if desc else ''}"
f"
"
)
return h
@@ -516,37 +587,53 @@ def _render_schedule(data: dict) -> str:
slots = data.get("time_slots", [])
label = html_mod.escape(data.get("label", data.get("day", "Schedule")))
- h = f'
{label}
'
+ h = f'
{label}
'
for slot in slots:
- h += f'
{slot.get("time", "")}
'
+ slot_time = html_mod.escape(slot.get("time", ""))
+ h += f'
{slot_time}
'
for s in slot.get("sessions", []):
title = html_mod.escape(s.get("title", ""))
room = html_mod.escape(s.get("room", ""))
stype = s.get("type", "")
+ start = html_mod.escape(s.get("start_time", ""))
+ end = html_mod.escape(s.get("end_time", ""))
sched_url = s.get("sched_url", "")
speakers = s.get("speakers", [])
+ is_keynote = stype == "keynote"
+ time_str = f"{start}–{end}" if start and end else start
# Title — linked to Sched if available
if sched_url:
title_html = (
- f'
{title} '
+ f'
{title} '
)
else:
- title_html = title
+ title_html = f'
{title} '
- # Speakers line
+ # Meta line: badge + time + room
+ meta_parts = []
+ if stype:
+ meta_parts.append(
+ f'
{html_mod.escape(stype.replace("_", " "))} '
+ )
+ if time_str:
+ meta_parts.append(f'
{time_str} ')
+ if room:
+ meta_parts.append(f'
{room} ')
+ meta_html = (
+ '
' + "".join(meta_parts) + "
" if meta_parts else ""
+ )
+
+ # Speakers
speakers_html = ""
if speakers and stype in ("keynote", "talk", "workshop"):
names = ", ".join(html_mod.escape(str(n)) for n in speakers)
- speakers_html = f'
{names}
'
+ speakers_html = f'
{names}
'
h += (
- f'
'
- f'{stype} '
- f'{title_html}{speakers_html} '
- f'{room}
'
+ f'
'
+ f"{title_html}{meta_html}{speakers_html}
"
)
return h
@@ -935,6 +1022,24 @@ def browse_sponsors(tier: str = "", query: str = "") -> dict:
return _browse_sponsors(upjack_app, tier=tier, query=query)
+@mcp.tool()
+def bookmark_session(
+ session_id: str,
+ priority: str = "want_to_attend",
+ notes: str = "",
+) -> dict:
+ """Bookmark a session for your personal schedule. Creates a bookmark entity linked to the session via a `bookmarks` relationship. Priority: must_attend, want_to_attend, maybe."""
+ if priority not in ("must_attend", "want_to_attend", "maybe"):
+ priority = "want_to_attend"
+ data: dict[str, Any] = {
+ "priority": priority,
+ "relationships": [{"rel": "bookmarks", "target": session_id}],
+ }
+ if notes:
+ data["notes"] = notes
+ return upjack_app.create_entity("bookmark", data)
+
+
# =============================================================================
# Local-only custom tools (planning & reports)
# =============================================================================
diff --git a/ui/dist/index.html b/ui/dist/index.html
index e4524d4..e715ad4 100644
--- a/ui/dist/index.html
+++ b/ui/dist/index.html
@@ -4,7 +4,7 @@
MCP Dev Summit
-
+.summit-link:hover { text-decoration: underline; }
+`,oo=[{value:"2026-04-01",short:"Wed",date:"Apr 1"},{value:"2026-04-02",short:"Thu",date:"Apr 2"},{value:"2026-04-03",short:"Fri",date:"Apr 3"}],ex=["agents","authentication","OpenAI","evals","tool use"],Wh=new Set(["break"]),tx=new Set(["talk","keynote","workshop","sponsor_activity"]);function Js(a){var o;const i=(o=a.relationships)==null?void 0:o.find(s=>s.rel==="bookmarks");return(i==null?void 0:i.target)??null}function fv(a){if(!a)return null;if(typeof a=="string")try{return JSON.parse(a)}catch{return a}if(typeof a=="object"&&a!==null){const i=a;if(Array.isArray(i.content)){const o=i.content.map(s=>s.text||"").join("");try{return JSON.parse(o)}catch{return o}}return i}return a}function Za(a){const i=fv(a);return i&&typeof i=="object"&&!Array.isArray(i)?i:{}}function nx(a){const i=fv(a);if(Array.isArray(i))return i;if(i&&typeof i=="object"){const o=i;if(Array.isArray(o.entities))return o.entities;if(Array.isArray(o.results))return o.results;if(Array.isArray(o.bookmarks))return o.bookmarks}return[]}function Ks(a){return["keynote","workshop","social","sponsor_activity"].includes(a)?`summit-badge summit-badge--${a}`:"summit-badge"}function ax(a){return a==="must_attend"?"summit-bk-row__priority summit-bk-row__priority--must":a==="want_to_attend"?"summit-bk-row__priority summit-bk-row__priority--want":"summit-bk-row__priority"}function co(a,i){return a?i?`${a}–${i}`:a:""}const Ih=()=>S.jsxs("svg",{className:"summit-search__icon",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",children:[S.jsx("circle",{cx:"7",cy:"7",r:"5"}),S.jsx("path",{d:"m11 11 3 3",strokeLinecap:"round"})]}),$s=()=>S.jsx("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.5",width:"14",height:"14",children:S.jsx("path",{d:"m4 4 8 8M12 4l-8 8",strokeLinecap:"round"})}),Ph=()=>S.jsx("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"2.2",children:S.jsx("path",{d:"m3 8 3 3 7-7",strokeLinecap:"round",strokeLinejoin:"round"})}),ep=()=>S.jsx("svg",{viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.8",children:S.jsx("path",{d:"M8 3v10M3 8h10",strokeLinecap:"round"})});function so({count:a=5}){return S.jsx("div",{children:Array.from({length:a}).map((i,o)=>S.jsxs("div",{className:"summit-skeleton-row",children:[S.jsx("div",{className:"summit-skeleton-row__line summit-skeleton",style:{width:`${50+Math.random()*40}%`}}),S.jsx("div",{className:"summit-skeleton-row__line summit-skeleton-row__line--short summit-skeleton"})]},o))})}function lx(){const a=F2(),i=I2();te.useEffect(()=>{document.documentElement.setAttribute("data-theme",i.mode)},[i.mode]);const[o,s]=te.useState("schedule"),[r,m]=te.useState("2026-04-02"),[h,v]=te.useState(""),[y,p]=te.useState(""),[x,A]=te.useState("all"),[H,X]=te.useState(null),P=da("get_day_schedule"),F=da("find_sessions"),ae=da("find_speaker_profiles"),de=da("list_bookmarks"),pe=da("delete_bookmark"),Ae=da("get_session"),Oe=da("bookmark_session"),je=da("get_speaker"),[_e,oe]=te.useState([]),[Y,V]=te.useState(""),[Se,Ie]=te.useState([]),[Pe,_n]=te.useState(new Map),[Mt,Bt]=te.useState([]),[C,L]=te.useState([]),[W,we]=te.useState(0),[Re,_]=te.useState(new Set),[Z,B]=te.useState(null),[Q,le]=te.useState(!1),[se,ke]=te.useState([]),dt=te.useCallback(async()=>{try{const w=await P.call({day:r}),ne=Za(w.data);oe(ne.time_slots||[]),V(ne.label||"")}catch{}},[r]),Qe=te.useCallback(async()=>{try{const w=await de.call({}),ve=Za(w.data).entities||nx(w.data);Ie(ve);const Ve=new Set;for(const he of ve){const He=Js(he);He&&Ve.add(He)}_(Ve);const fe=new Map,ge=Array.from(Ve).map(async he=>{try{const He=await Ae.call({session_id:he}),ee=Za(He.data);ee!=null&&ee.id&&fe.set(he,ee)}catch{}});await Promise.all(ge),_n(fe)}catch{}},[]);async function Bn(w){B(w),ke([]),le(!0);try{let ne=w;if(!w.description){const fe=await Ae.call({session_id:w.id}),ge=Za(fe.data);ge!=null&&ge.id&&(ne=ge,B(ge))}const Ve=(ne.relationships||[]).filter(fe=>fe.rel==="presented_by").map(fe=>fe.target);if(Ve.length>0){const fe=[];for(const ge of Ve)try{const he=await je.call({speaker_id:ge}),He=Za(he.data);He!=null&&He.name&&fe.push(He)}catch{}ke(fe)}}catch{}le(!1)}const pn=te.useRef(null),Ln=te.useRef(0),[ya,fn]=te.useState(!1);te.useEffect(()=>{if(pn.current&&clearTimeout(pn.current),!h.trim()){L([]),we(0),fn(!1);return}return pn.current=setTimeout(async()=>{const w=++Ln.current;fn(!0);try{const ne=await F.call({query:h,limit:30});if(w!==Ln.current)return;const ve=Za(ne.data);L(ve.results||[]),we(ve.total||0)}catch(ne){if(w!==Ln.current)return;console.error("[summit] search error:",ne)}finally{w===Ln.current&&fn(!1)}},250),()=>{pn.current&&clearTimeout(pn.current)}},[h]);const Sn=te.useRef(null),Ha=te.useRef(0);te.useEffect(()=>{if(o==="speakers")return Sn.current&&clearTimeout(Sn.current),Sn.current=setTimeout(async()=>{const w=++Ha.current;try{const ne={limit:200};y.trim()&&(ne.query=y),x==="keynote"&&(ne.is_keynote=!0);const ve=await ae.call(ne);if(w!==Ha.current)return;const Ve=Za(ve.data);Bt(Ve.results||[])}catch{}},250),()=>{Sn.current&&clearTimeout(Sn.current)}},[o,y,x]);async function Rl(w){if(Re.has(w)){const ne=Se.find(ve=>Js(ve)===w);if(ne)try{await pe.call({bookmark_id:ne.id})}catch{}}else try{await Oe.call({session_id:w})}catch{}await Qe()}te.useEffect(()=>{dt()},[r,dt]),te.useEffect(()=>{Qe()},[Qe]),W2(()=>{dt(),Qe()}),te.useEffect(()=>{if(!Z)return;const w=ne=>{ne.key==="Escape"&&B(null)};return window.addEventListener("keydown",w),()=>window.removeEventListener("keydown",w)},[Z]);function Cl({session:w}){var he;const ne=Re.has(w.id),ve=w.session_type||w.type||"",Ve=tx.has(ve),fe=ve==="keynote",ge=((he=w.speakers)==null?void 0:he.map(He=>He.name))||w.speaker_names||[];return S.jsxs("div",{className:"summit-row","data-clickable":Ve,"data-bookmarked":ne&&!fe,"data-keynote":fe,onClick:()=>Ve&&Bn({...w,session_type:ve}),children:[S.jsx("button",{type:"button",className:"summit-bk","aria-pressed":ne,"aria-label":ne?"Remove bookmark":"Add bookmark",title:ne?"Remove bookmark":"Bookmark this session",onClick:He=>{He.stopPropagation(),Rl(w.id)},children:ne?S.jsx(Ph,{}):S.jsx(ep,{})}),S.jsxs("div",{children:[S.jsx("div",{className:"summit-row__title",children:w.title}),S.jsxs("div",{className:"summit-row__meta",children:[S.jsx("span",{className:Ks(ve),children:ve.replace(/_/g," ")}),w.room&&S.jsx("span",{className:"summit-row__meta-room",children:w.room}),w.start_time&&S.jsx("span",{className:"summit-row__meta-time",children:co(w.start_time,w.end_time)})]}),ge.length>0&&S.jsx("div",{className:"summit-row__speakers",children:ge.join(", ")})]})]})}function Ba({session:w}){return S.jsxs("div",{className:"summit-break",children:[S.jsx("span",{className:"summit-break__time",children:w.start_time?co(w.start_time,w.end_time):""}),S.jsx("span",{children:w.title}),w.room&&S.jsxs("span",{style:{color:"var(--summit-ink-3)"},children:["· ",w.room]})]})}function Dl(){var he,He;if(!Z)return null;const w=Z,ne=Re.has(w.id),ve=((he=w.speakers)==null?void 0:he.map(ee=>ee.name))||w.speaker_names||[],Ve=((He=w.speakers)==null?void 0:He.map(ee=>ee.company))||w.speaker_companies||[],fe=oo.find(ee=>ee.value===w.day),ge=fe?`${fe.short} ${fe.date}`:w.day;return S.jsx("div",{className:"summit-modal__overlay",onClick:()=>B(null),children:S.jsxs("div",{className:"summit-modal",onClick:ee=>ee.stopPropagation(),role:"dialog","aria-modal":"true",children:[S.jsxs("div",{className:"summit-modal__head",children:[S.jsxs("div",{className:"summit-modal__eyebrow",children:[S.jsx("span",{className:Ks(w.session_type),children:w.session_type.replace(/_/g," ")}),w.track&&w.track!=="keynote"&&w.track!=="special_events"&&S.jsx("span",{className:"summit-badge summit-badge--track",children:w.track.replace(/_/g," ")})]}),S.jsx("h2",{className:"summit-modal__title",children:w.title}),S.jsx("button",{type:"button",className:"summit-modal__close","aria-label":"Close",onClick:()=>B(null),children:S.jsx($s,{})})]}),S.jsxs("div",{className:"summit-modal__body",children:[S.jsxs("div",{className:"summit-modal__row",children:[S.jsxs("div",{className:"summit-modal__cell",children:[S.jsx("div",{className:"summit-label",children:"When"}),S.jsxs("div",{className:"summit-modal__when",children:[ge," · ",co(w.start_time,w.end_time)]})]}),w.room&&S.jsxs("div",{className:"summit-modal__cell",children:[S.jsx("div",{className:"summit-label",children:"Where"}),S.jsx("div",{className:"summit-modal__when",children:w.room})]})]}),(se.length>0||ve.length>0)&&S.jsxs("div",{className:"summit-modal__section",children:[S.jsx("div",{className:"summit-label",children:"Speakers"}),se.length>0?se.map(ee=>S.jsxs("div",{className:"summit-modal__speaker",children:[ee.photo_url?S.jsx("img",{src:ee.photo_url,alt:ee.name}):S.jsx("div",{className:"summit-spk__initial",children:ee.name.charAt(0)}),S.jsxs("div",{children:[S.jsxs("div",{className:"summit-modal__sp-name",children:[S.jsx("span",{children:ee.name}),ee.linkedin_url&&S.jsx("a",{href:ee.linkedin_url,className:"summit-link",onClick:At=>{At.preventDefault(),At.stopPropagation(),ee.linkedin_url&&a.openLink(ee.linkedin_url)},children:"LinkedIn"})]}),S.jsxs("div",{className:"summit-modal__sp-role",children:[ee.role?`${ee.role}, `:"",ee.company]}),ee.bio&&S.jsx("div",{className:"summit-modal__sp-bio",children:ee.bio.length>220?ee.bio.substring(0,220)+"…":ee.bio})]})]},ee.id)):ve.map((ee,At)=>S.jsxs("div",{className:"summit-modal__sp-name",style:{padding:"4px 0"},children:[ee,Ve[At]&&S.jsxs("span",{style:{fontWeight:400,color:"var(--summit-ink-2)"},children:[" ","· ",Ve[At]]})]},At))]}),Q?S.jsx("div",{style:{color:"var(--summit-ink-3)",fontSize:13},children:"Loading details…"}):w.description?S.jsxs("div",{className:"summit-modal__section",children:[S.jsx("div",{className:"summit-label",children:"About"}),S.jsx("div",{className:"summit-modal__desc",children:w.description})]}):null]}),S.jsxs("div",{className:"summit-modal__actions",children:[S.jsx("button",{type:"button",className:ne?"summit-btn summit-btn--unbookmark":"summit-btn summit-btn--primary",onClick:()=>Rl(w.id),children:ne?"Remove bookmark":"Bookmark session"}),w.sched_url&&S.jsx("button",{type:"button",className:"summit-btn summit-btn--ghost",onClick:()=>w.sched_url&&a.openLink(w.sched_url),children:"Sched ↗"})]})]})})}function Ul(){return S.jsxs(S.Fragment,{children:[S.jsx("div",{className:"summit-days",role:"tablist","aria-label":"Conference day",children:oo.map(w=>S.jsxs("button",{type:"button",className:"summit-days__btn","aria-pressed":r===w.value,onClick:()=>m(w.value),children:[S.jsx("span",{className:"summit-days__btn--day",children:w.short}),S.jsx("span",{className:"summit-days__btn--date",children:w.date})]},w.value))}),P.isPending&&_e.length===0?S.jsx(so,{count:6}):_e.length===0?S.jsxs("div",{className:"summit-empty",children:[S.jsx("div",{className:"summit-empty__icon",children:"∅"}),S.jsx("div",{className:"summit-empty__title",children:"No sessions"}),S.jsx("div",{className:"summit-empty__hint",children:"Nothing on the schedule for this day yet."})]}):_e.map(w=>{const ne=fe=>fe.session_type||fe.type||"",ve=w.sessions.filter(fe=>Wh.has(ne(fe))),Ve=w.sessions.filter(fe=>!Wh.has(ne(fe)));return Ve.length===0&&ve.length>0?S.jsx("div",{children:ve.map(fe=>S.jsx(Ba,{session:fe},fe.id))},w.time):S.jsxs("div",{className:"summit-slot",children:[S.jsxs("div",{className:"summit-slot__head",children:[S.jsx("span",{className:"summit-slot__time",children:w.time}),S.jsx("span",{className:"summit-slot__rule"}),S.jsxs("span",{className:"summit-slot__count",children:[Ve.length," ",Ve.length===1?"session":"sessions"]})]}),Ve.map(fe=>S.jsx(Cl,{session:fe},fe.id)),ve.map(fe=>S.jsx(Ba,{session:fe},fe.id))]},w.time)}),Y&&S.jsx("div",{style:{fontSize:11,color:"var(--summit-ink-3)",textAlign:"center",marginTop:16},children:Y})]})}function xo(){const w=Se.map(ge=>{const he=Js(ge),He=he?Pe.get(he):null;return{bk:ge,session:He,sessionId:he}}),ne=w.filter(ge=>{var he;return(he=ge.session)==null?void 0:he.day}),ve=w.filter(ge=>{var he;return!((he=ge.session)!=null&&he.day)}),Ve=oo.map(ge=>({...ge,items:ne.filter(he=>he.session.day===ge.value)})).filter(ge=>ge.items.length>0),fe=({bk:ge,session:he,sessionId:He})=>S.jsxs("div",{className:"summit-bk-row",onClick:()=>he&&Bn(he),children:[S.jsxs("div",{children:[S.jsxs("div",{className:"summit-bk-row__title",children:[S.jsx("span",{className:ax(ge.priority||"want_to_attend")}),(he==null?void 0:he.title)||He||"(missing session)"]}),he&&S.jsxs("div",{className:"summit-bk-row__meta",children:[he.start_time&&co(he.start_time,he.end_time),he.room&&` · ${he.room}`]})]}),S.jsx("button",{type:"button",className:"summit-bk","aria-pressed":!0,"aria-label":"Remove bookmark",title:"Remove bookmark",onClick:ee=>{ee.stopPropagation(),He&&Rl(He)},children:S.jsx(Ph,{})})]},ge.id);return de.isPending&&Se.length===0?S.jsx(so,{count:4}):Se.length===0?S.jsxs("div",{className:"summit-empty",children:[S.jsx("div",{className:"summit-empty__icon",children:"★"}),S.jsx("div",{className:"summit-empty__title",children:"No bookmarks yet"}),S.jsxs("div",{className:"summit-empty__hint",children:["Tap ",S.jsx("span",{style:{display:"inline-block",verticalAlign:"middle",margin:"0 2px"},children:S.jsx("span",{className:"summit-bk",style:{width:18,height:18,display:"inline-flex"},children:S.jsx(ep,{})})})," next to any session to save it here."]})]}):S.jsxs(S.Fragment,{children:[Ve.map(ge=>S.jsxs("div",{children:[S.jsxs("div",{className:"summit-day-head",children:[S.jsxs("span",{children:[ge.short," · ",ge.date]}),S.jsx("span",{className:"summit-day-head__count",children:ge.items.length})]}),ge.items.map(fe)]},ge.value)),ve.length>0&&S.jsxs("div",{children:[Ve.length>0&&S.jsxs("div",{className:"summit-day-head",children:[S.jsx("span",{children:"Other"}),S.jsx("span",{className:"summit-day-head__count",children:ve.length})]}),ve.map(fe)]})]})}function To(){return S.jsxs(S.Fragment,{children:[S.jsxs("div",{className:"summit-search",children:[S.jsx(Ih,{}),S.jsx("input",{className:"summit-search__input",type:"search",placeholder:"Search speakers",value:y,onChange:w=>p(w.target.value)}),y&&S.jsx("button",{type:"button",className:"summit-search__clear",onClick:()=>p(""),"aria-label":"Clear search",children:S.jsx($s,{})})]}),S.jsxs("div",{className:"summit-filters",children:[S.jsx("button",{type:"button",className:"summit-filter","aria-pressed":x==="all",onClick:()=>A("all"),children:"All"}),S.jsx("button",{type:"button",className:"summit-filter","aria-pressed":x==="keynote",onClick:()=>A("keynote"),children:"Keynotes"})]}),ae.isPending&&Mt.length===0?S.jsx(so,{count:6}):Mt.length===0?S.jsxs("div",{className:"summit-empty",children:[S.jsx("div",{className:"summit-empty__icon",children:"⌖"}),S.jsx("div",{className:"summit-empty__title",children:"No speakers found"}),S.jsx("div",{className:"summit-empty__hint",children:"Try a different search or clear the filter."})]}):S.jsxs(S.Fragment,{children:[S.jsxs("div",{className:"summit-speakers__count",children:[Mt.length," speakers"]}),Mt.map(w=>S.jsxs("div",{className:"summit-spk",onClick:()=>X(H===w.id?null:w.id),children:[S.jsxs("div",{className:"summit-spk__head",children:[w.photo_url?S.jsx("img",{className:"summit-spk__avatar",src:w.photo_url,alt:w.name}):S.jsx("div",{className:"summit-spk__initial",children:w.name.charAt(0)}),S.jsxs("div",{children:[S.jsxs("div",{className:"summit-spk__name",children:[w.name,w.is_keynote&&S.jsx("span",{className:Ks("keynote"),children:"keynote"}),w.linkedin_url&&S.jsx("a",{href:w.linkedin_url,target:"_blank",rel:"noopener",className:"summit-link",onClick:ne=>ne.stopPropagation(),children:"LinkedIn"})]}),S.jsxs("div",{className:"summit-spk__role",children:[w.role?`${w.role}, `:"",w.company]}),w.topics&&w.topics.length>0&&S.jsxs("div",{className:"summit-spk__topics",children:[w.topics.slice(0,4).map((ne,ve)=>S.jsx("span",{className:"summit-spk__topic",children:ne},ve)),w.topics.length>4&&S.jsxs("span",{className:"summit-spk__topic",style:{opacity:.6},children:["+",w.topics.length-4]})]})]})]}),H===w.id&&S.jsxs("div",{className:"summit-spk__expand",children:[w.bio&&S.jsx("div",{className:"summit-spk__bio",children:w.bio}),w.sessions&&w.sessions.length>0&&S.jsxs("div",{children:[S.jsx("div",{className:"summit-label",style:{marginBottom:6},children:"Sessions"}),S.jsx("div",{className:"summit-spk__sess-list",children:w.sessions.map(ne=>{const ve=oo.find(Ve=>Ve.value===ne.day);return S.jsxs("div",{className:"summit-spk__sess-item",children:[S.jsx("span",{className:"summit-spk__sess-time",children:ve?`${ve.short} ${ne.start_time}`:ne.start_time}),S.jsx("span",{children:ne.title})]},ne.id)})})]})]})]},w.id))]})]})}function kt(){return S.jsxs(S.Fragment,{children:[S.jsxs("div",{className:"summit-search",children:[S.jsx(Ih,{}),S.jsx("input",{className:"summit-search__input",type:"search",placeholder:"Search sessions, speakers, topics…",value:h,onChange:w=>v(w.target.value),autoFocus:!0}),h&&S.jsx("button",{type:"button",className:"summit-search__clear",onClick:()=>v(""),"aria-label":"Clear search",children:S.jsx($s,{})})]}),!h.trim()&&S.jsxs(S.Fragment,{children:[S.jsx("div",{className:"summit-suggestions",children:ex.map(w=>S.jsx("button",{type:"button",className:"summit-suggest",onClick:()=>v(w),children:w},w))}),S.jsxs("div",{className:"summit-empty",children:[S.jsx("div",{className:"summit-empty__icon",children:"⌕"}),S.jsx("div",{className:"summit-empty__title",children:"Search the conference"}),S.jsx("div",{className:"summit-empty__hint",children:"Find sessions by topic, speaker, company, or keyword."})]})]}),ya&&S.jsx(so,{count:4}),!ya&&h.trim()&&W>0&&S.jsxs("div",{className:"summit-speakers__count",children:[W," results"]}),!ya&&h.trim()&&C.length===0&&S.jsxs("div",{className:"summit-empty",children:[S.jsx("div",{className:"summit-empty__icon",children:"∅"}),S.jsxs("div",{className:"summit-empty__title",children:['No matches for "',h,'"']}),S.jsx("div",{className:"summit-empty__hint",children:"Try a broader term or check spelling."})]}),!ya&&C.map(w=>S.jsx(Cl,{session:w},w.id))]})}const Eo=te.useMemo(()=>[{key:"schedule",label:"Schedule"},{key:"bookmarks",label:"Bookmarks",count:Se.length},{key:"speakers",label:"Speakers"},{key:"search",label:"Search"}],[Se.length]);return S.jsxs(S.Fragment,{children:[S.jsx("style",{children:P2}),S.jsx("div",{className:"summit-root",children:S.jsxs("div",{className:"summit-shell",children:[S.jsx("nav",{className:"summit-nav",role:"tablist",children:Eo.map(w=>S.jsxs("button",{type:"button",role:"tab",className:"summit-nav__tab","aria-selected":o===w.key,onClick:()=>s(w.key),children:[w.label,w.count!==void 0&&w.count>0&&S.jsx("span",{className:"summit-nav__count",children:w.count})]},w.key))}),o==="schedule"&&S.jsx(Ul,{}),o==="bookmarks"&&S.jsx(xo,{}),o==="speakers"&&S.jsx(To,{}),o==="search"&&S.jsx(kt,{}),Dl()]})})]})}function ix(){return S.jsx(K2,{name:"mcp-dev-summit",version:"0.6.0",children:S.jsx(lx,{})})}Hy.createRoot(document.getElementById("root")).render(S.jsx(ix,{}));
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 3606ceb..70635a2 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -6,7 +6,7 @@
"": {
"name": "mcp-dev-summit-ui",
"dependencies": {
- "@nimblebrain/synapse": "^0.3.0",
+ "@nimblebrain/synapse": "^0.8.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@@ -875,9 +875,9 @@
}
},
"node_modules/@nimblebrain/synapse": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@nimblebrain/synapse/-/synapse-0.3.0.tgz",
- "integrity": "sha512-NNWKFKkDE1K29AN8ANwSH5qoXn6F5wkfxUaSG6tavGrUVoTlOYr3bLzPP1NRZGc6+MlYK+e1BwrA9wDKbqJyoA==",
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/@nimblebrain/synapse/-/synapse-0.8.0.tgz",
+ "integrity": "sha512-he6+9AOZFsZwnQS6IGbBrkL5OoQ1AwTpjACNQnyyqXgljZ/BhXNCSG9CzzUtS+XorYcZ4EZIEadUXFXSafZkZw==",
"license": "MIT",
"bin": {
"synapse": "dist/codegen/cli.js"
diff --git a/ui/package.json b/ui/package.json
index 4c57a58..ad87eea 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -8,7 +8,7 @@
"preview": "vite preview"
},
"dependencies": {
- "@nimblebrain/synapse": "^0.3.0",
+ "@nimblebrain/synapse": "^0.8.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index dbf3d15..0092c5e 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -1,10 +1,12 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
SynapseProvider,
useCallTool,
useDataSync,
useSynapse,
+ useTheme,
} from "@nimblebrain/synapse/react";
+import { SUMMIT_CSS } from "./styles";
/* ---------- types ---------- */
@@ -55,283 +57,21 @@ interface Speaker {
topics: string[];
is_keynote: boolean;
sessions: { id: string; title: string; day: string; start_time: string }[];
+ linkedin_url?: string;
}
-/* ---------- tabs ---------- */
-
type Tab = "schedule" | "bookmarks" | "speakers" | "search";
const DAYS = [
- { value: "2026-04-01", label: "Wed Apr 1" },
- { value: "2026-04-02", label: "Thu Apr 2" },
- { value: "2026-04-03", label: "Fri Apr 3" },
+ { value: "2026-04-01", short: "Wed", date: "Apr 1" },
+ { value: "2026-04-02", short: "Thu", date: "Apr 2" },
+ { value: "2026-04-03", short: "Fri", date: "Apr 3" },
];
-/* ---------- CSS ---------- */
+const SEARCH_SUGGESTIONS = ["agents", "authentication", "OpenAI", "evals", "tool use"];
-const SUMMIT_CSS = `
-.summit-container {
- background: var(--color-background-primary, #0f172a);
- color: var(--color-text-primary, #e2e8f0);
- font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
- min-height: 100vh;
- padding: 0.75rem;
-}
-.summit-tabs {
- display: flex;
- gap: 2px;
- margin-bottom: 0.75rem;
- background: var(--color-background-secondary, #1e293b);
- border-radius: var(--border-radius-sm, 0.5rem);
- padding: 3px;
-}
-.summit-tab {
- flex: 1;
- padding: 0.5rem;
- border: none;
- border-radius: var(--border-radius-sm, 0.5rem);
- background: transparent;
- color: var(--color-text-secondary, #94a3b8);
- font-size: 0.8rem;
- font-weight: 400;
- cursor: pointer;
- transition: all 0.15s;
-}
-.summit-tab--active {
- background: var(--color-text-accent, #818cf8);
- color: var(--nb-color-accent-foreground, #ffffff);
- font-weight: 600;
-}
-.summit-day-picker {
- display: flex;
- gap: 4px;
- margin-bottom: 0.75rem;
-}
-.summit-day-btn {
- flex: 1;
- padding: 0.4rem;
- border: 1px solid var(--color-border-primary, #334155);
- border-radius: var(--border-radius-sm, 0.5rem);
- background: transparent;
- color: var(--color-text-primary, #e2e8f0);
- font-size: 0.75rem;
- cursor: pointer;
- font-weight: 400;
-}
-.summit-day-btn--active {
- border-color: var(--color-text-accent, #818cf8);
- background: var(--color-text-accent, #818cf8);
- color: var(--nb-color-accent-foreground, #ffffff);
- font-weight: 600;
-}
-.summit-card {
- background: var(--color-background-secondary, #1e293b);
- border: 1px solid var(--color-border-primary, #334155);
- border-radius: var(--border-radius-sm, 0.5rem);
- padding: 0.75rem;
- margin-bottom: 0.5rem;
-}
-.summit-card--clickable {
- cursor: pointer;
- transition: border-color 0.15s;
-}
-.summit-card--clickable:hover {
- border-color: var(--color-text-accent, #818cf8);
-}
-.summit-session-title {
- font-size: 0.85rem;
- font-weight: 500;
- color: var(--color-text-primary, #e2e8f0);
- margin-bottom: 2px;
-}
-.summit-session-meta {
- font-size: 0.7rem;
- color: var(--color-text-secondary, #94a3b8);
-}
-.summit-time-slot-header {
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--color-text-accent, #818cf8);
- padding: 0.5rem 0 0.25rem;
- border-bottom: 1px solid var(--color-border-primary, #334155);
- margin-bottom: 0.5rem;
-}
-.summit-bookmark-btn {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 1.1rem;
- padding: 2px 4px;
- color: var(--color-text-secondary, #94a3b8);
- opacity: 0.5;
- transition: all 0.15s;
-}
-.summit-bookmark-btn--active {
- color: #eab308;
- opacity: 1;
-}
-.summit-input {
- flex: 1;
- padding: 0.5rem 0.6rem;
- border-radius: var(--border-radius-sm, 0.5rem);
- border: 1px solid var(--color-border-primary, #334155);
- background: var(--color-background-secondary, #1e293b);
- color: var(--color-text-primary, #e2e8f0);
- font-size: 0.85rem;
- outline: none;
-}
-.summit-btn {
- padding: 0.5rem 1rem;
- border-radius: var(--border-radius-sm, 0.5rem);
- border: none;
- background: var(--color-text-accent, #818cf8);
- color: var(--nb-color-accent-foreground, #ffffff);
- font-size: 0.85rem;
- font-weight: 500;
- cursor: pointer;
-}
-.summit-btn--outline {
- background: transparent;
- border: 1px solid var(--color-border-primary, #334155);
- color: var(--color-text-primary, #e2e8f0);
-}
-.summit-btn--unbookmark {
- background: transparent;
- border: 1px solid var(--color-border-primary, #334155);
- color: var(--color-text-secondary, #94a3b8);
-}
-.summit-badge {
- display: inline-block;
- padding: 1px 6px;
- border-radius: 3px;
- font-size: 0.6rem;
- font-weight: 600;
- text-transform: uppercase;
- margin-right: 4px;
- background: #64748b22;
- color: #64748b;
-}
-.summit-badge--keynote { background: #eab30822; color: #eab308; }
-.summit-badge--talk { background: #6366f122; color: #6366f1; }
-.summit-badge--workshop { background: #22c55e22; color: #22c55e; }
-.summit-badge--break { background: #64748b22; color: #64748b; }
-.summit-badge--social { background: #ec489922; color: #ec4899; }
-.summit-badge--sponsor_activity { background: #f9731622; color: #f97316; }
-.summit-badge--track {
- background: color-mix(in srgb, var(--color-text-accent, #818cf8) 13%, transparent);
- color: var(--color-text-accent, #818cf8);
-}
-.summit-priority-dot {
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #64748b;
- margin-right: 6px;
-}
-.summit-priority-dot--must { background: #ef4444; }
-.summit-priority-dot--want { background: #eab308; }
-.summit-empty {
- text-align: center;
- padding: 2rem;
- color: var(--color-text-secondary, #94a3b8);
- font-size: 0.85rem;
-}
-.summit-modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0,0,0,0.6);
- display: flex;
- align-items: flex-end;
- justify-content: center;
- z-index: 1000;
- padding: 1rem;
-}
-.summit-modal {
- background: var(--color-background-secondary, #1e293b);
- border: 1px solid var(--color-border-primary, #334155);
- border-radius: 12px 12px 0 0;
- padding: 1.25rem;
- width: 100%;
- max-width: 600px;
- max-height: 80vh;
- overflow-y: auto;
- color: var(--color-text-primary, #e2e8f0);
-}
-.summit-modal-close {
- background: none;
- border: none;
- color: var(--color-text-secondary, #94a3b8);
- font-size: 1.5rem;
- cursor: pointer;
- padding: 0 0.25rem;
- line-height: 1;
-}
-.summit-label {
- color: var(--color-text-secondary, #94a3b8);
- font-size: 0.7rem;
- text-transform: uppercase;
- margin-bottom: 2px;
-}
-.summit-speaker-photo {
- width: 44px;
- height: 44px;
- border-radius: 50%;
- object-fit: cover;
- flex-shrink: 0;
-}
-.summit-speaker-photo--lg {
- width: 48px;
- height: 48px;
-}
-.summit-speaker-avatar {
- width: 44px;
- height: 44px;
- border-radius: 50%;
- background: color-mix(in srgb, var(--color-text-accent, #818cf8) 20%, transparent);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1rem;
- color: var(--color-text-accent, #818cf8);
- flex-shrink: 0;
-}
-.summit-speaker-avatar--lg {
- width: 48px;
- height: 48px;
- font-size: 1.1rem;
-}
-.summit-topic-tag {
- font-size: 0.6rem;
- padding: 1px 5px;
- border-radius: 3px;
- background: color-mix(in srgb, var(--color-text-accent, #818cf8) 8%, transparent);
- color: var(--color-text-accent, #818cf8);
- border: 1px solid color-mix(in srgb, var(--color-text-accent, #818cf8) 20%, transparent);
-}
-.summit-link {
- color: var(--color-text-accent, #818cf8);
- font-size: 0.7rem;
- text-decoration: none;
- cursor: pointer;
-}
-.summit-link:visited,
-.summit-link:active {
- color: var(--color-text-accent, #818cf8);
-}
-.summit-border-top {
- border-top: 1px solid var(--color-border-primary, #334155);
-}
-.summit-border-bottom {
- border-bottom: 1px solid color-mix(in srgb, var(--color-border-primary, #334155) 13%, transparent);
-}
-.summit-muted {
- color: var(--color-text-secondary, #94a3b8);
-}
-.summit-accent {
- color: var(--color-text-accent, #818cf8);
-}
-`;
+const BREAK_TYPES = new Set(["break"]);
+const CLICKABLE_TYPES = new Set(["talk", "keynote", "workshop", "sponsor_activity"]);
/* ---------- helpers ---------- */
@@ -369,7 +109,6 @@ function asDict(raw: unknown): Record
{
function asList(raw: unknown): unknown[] {
const parsed = parseToolResult(raw);
if (Array.isArray(parsed)) return parsed;
- // Upjack list tools return { entities: [...] } or just an array
if (parsed && typeof parsed === "object") {
const obj = parsed as Record;
if (Array.isArray(obj.entities)) return obj.entities;
@@ -379,24 +118,76 @@ function asList(raw: unknown): unknown[] {
return [];
}
-/* ---------- badge class helper ---------- */
-
function badgeClass(type: string): string {
- const known = ["keynote", "talk", "workshop", "break", "social", "sponsor_activity"];
+ const known = ["keynote", "workshop", "social", "sponsor_activity"];
if (known.includes(type)) return `summit-badge summit-badge--${type}`;
return "summit-badge";
}
-function priorityDotClass(p: string): string {
- if (p === "must_attend") return "summit-priority-dot summit-priority-dot--must";
- if (p === "want_to_attend") return "summit-priority-dot summit-priority-dot--want";
- return "summit-priority-dot";
+function priorityClass(p: string): string {
+ if (p === "must_attend") return "summit-bk-row__priority summit-bk-row__priority--must";
+ if (p === "want_to_attend") return "summit-bk-row__priority summit-bk-row__priority--want";
+ return "summit-bk-row__priority";
+}
+
+function fmtTimeRange(start: string, end: string): string {
+ if (!start) return "";
+ if (!end) return start;
+ return `${start}–${end}`;
+}
+
+/* ---------- icons ---------- */
+
+const SearchIcon = () => (
+
+
+
+
+);
+
+const CloseIcon = () => (
+
+
+
+);
+
+const CheckIcon = () => (
+
+
+
+);
+
+const PlusIcon = () => (
+
+
+
+);
+
+/* ---------- skeleton ---------- */
+
+function SkeletonRows({ count = 5 }: { count?: number }) {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
}
-/* ---------- main app ---------- */
+/* ---------- main ---------- */
function SummitUI() {
const synapse = useSynapse();
+ const theme = useTheme();
+
+ // Apply theme mode to root element so the CSS palette responds.
+ useEffect(() => {
+ document.documentElement.setAttribute("data-theme", theme.mode);
+ }, [theme.mode]);
const [tab, setTab] = useState("schedule");
const [day, setDay] = useState("2026-04-02");
@@ -405,16 +196,15 @@ function SummitUI() {
const [speakerFilter, setSpeakerFilter] = useState<"all" | "keynote">("all");
const [expandedSpeaker, setExpandedSpeaker] = useState(null);
- // Tool hooks — custom tools (our format)
+ // Tools
const scheduleTool = useCallTool("get_day_schedule");
const searchTool = useCallTool("find_sessions");
const speakersTool = useCallTool("find_speaker_profiles");
-
- // Tool hooks — Upjack auto-generated (entity format)
const listBookmarksTool = useCallTool("list_bookmarks");
- const createBookmarkTool = useCallTool("create_bookmark");
const deleteBookmarkTool = useCallTool("delete_bookmark");
const getSessionTool = useCallTool("get_session");
+ const bookmarkSessionTool = useCallTool("bookmark_session");
+ const getSpeakerTool = useCallTool("get_speaker");
// State
const [schedule, setSchedule] = useState([]);
@@ -429,44 +219,7 @@ function SummitUI() {
const [detailLoading, setDetailLoading] = useState(false);
const [detailSpeakers, setDetailSpeakers] = useState([]);
- const getRelatedTool = useCallTool("get_related_session");
- const getSpeakerTool = useCallTool("get_speaker");
-
- async function openSessionDetail(session: Session) {
- setSelectedSession(session);
- setDetailSpeakers([]);
- setDetailLoading(true);
-
- try {
- // Fetch full session if we don't have description
- let full = session;
- if (!session.description) {
- const result = await getSessionTool.call({ session_id: session.id });
- const fetched = asDict(result.data) as unknown as Session;
- if (fetched?.id) {
- full = fetched;
- setSelectedSession(fetched);
- }
- }
-
- // Fetch speakers via relationships
- const rels = (full as Record).relationships as Relationship[] | undefined;
- const speakerTargets = (rels || []).filter(r => r.rel === "presented_by").map(r => r.target);
-
- if (speakerTargets.length > 0) {
- const fetched: Speaker[] = [];
- for (const sid of speakerTargets) {
- try {
- const r = await getSpeakerTool.call({ speaker_id: sid });
- const sp = asDict(r.data) as unknown as Speaker;
- if (sp?.name) fetched.push(sp);
- } catch { /* */ }
- }
- setDetailSpeakers(fetched);
- }
- } catch { /* keep partial */ }
- setDetailLoading(false);
- }
+ /* ---------- data fetching ---------- */
const loadSchedule = useCallback(async () => {
try {
@@ -475,6 +228,9 @@ function SummitUI() {
setSchedule((data.time_slots as TimeSlot[]) || []);
setScheduleLabel((data.label as string) || "");
} catch { /* */ }
+ // scheduleTool is intentionally not in deps — useCallTool returns a fresh
+ // reference each render which would cause infinite loops.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [day]);
const loadBookmarks = useCallback(async () => {
@@ -491,7 +247,6 @@ function SummitUI() {
}
setBookmarkedSessionIds(sessionIds);
- // Fetch session details in parallel
const sessionMap = new Map();
const fetches = Array.from(sessionIds).map(async (sid) => {
try {
@@ -503,24 +258,48 @@ function SummitUI() {
await Promise.all(fetches);
setBookmarkSessions(sessionMap);
} catch { /* */ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const loadSpeakers = useCallback(async () => {
+ /* ---------- bookmark detail ---------- */
+
+ async function openSessionDetail(session: Session) {
+ setSelectedSession(session);
+ setDetailSpeakers([]);
+ setDetailLoading(true);
try {
- const args: Record = { limit: 200 };
- if (speakerQuery.trim()) args.query = speakerQuery;
- if (speakerFilter === "keynote") args.is_keynote = true;
- const result = await speakersTool.call(args);
- const data = asDict(result.data);
- setSpeakers((data.results as Speaker[]) || []);
+ let full = session;
+ if (!session.description) {
+ const result = await getSessionTool.call({ session_id: session.id });
+ const fetched = asDict(result.data) as unknown as Session;
+ if (fetched?.id) {
+ full = fetched;
+ setSelectedSession(fetched);
+ }
+ }
+ const rels = (full as unknown as Record).relationships as Relationship[] | undefined;
+ const speakerTargets = (rels || []).filter(r => r.rel === "presented_by").map(r => r.target);
+ if (speakerTargets.length > 0) {
+ const fetched: Speaker[] = [];
+ for (const sid of speakerTargets) {
+ try {
+ const r = await getSpeakerTool.call({ speaker_id: sid });
+ const sp = asDict(r.data) as unknown as Speaker;
+ if (sp?.name) fetched.push(sp);
+ } catch { /* */ }
+ }
+ setDetailSpeakers(fetched);
+ }
} catch { /* */ }
- }, [speakerQuery, speakerFilter]);
+ setDetailLoading(false);
+ }
+
+ /* ---------- search ---------- */
const searchTimerRef = useRef | null>(null);
const searchSeqRef = useRef(0);
const [searching, setSearching] = useState(false);
- // Debounced auto-search on query change
useEffect(() => {
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
if (!searchQuery.trim()) {
@@ -534,7 +313,7 @@ function SummitUI() {
setSearching(true);
try {
const result = await searchTool.call({ query: searchQuery, limit: 30 });
- if (seq !== searchSeqRef.current) return; // stale — newer search in flight
+ if (seq !== searchSeqRef.current) return;
const data = asDict(result.data);
setSearchResults((data.results as Session[]) || []);
setSearchTotal((data.total as number) || 0);
@@ -544,34 +323,16 @@ function SummitUI() {
} finally {
if (seq === searchSeqRef.current) setSearching(false);
}
- }, 600);
+ }, 250);
return () => { if (searchTimerRef.current) clearTimeout(searchTimerRef.current); };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery]);
- async function toggleBookmark(sessionId: string) {
- if (bookmarkedSessionIds.has(sessionId)) {
- const bk = bookmarks.find((b) => getSessionIdFromBookmark(b) === sessionId);
- if (bk) {
- try { await deleteBookmarkTool.call({ bookmark_id: bk.id }); } catch { /* */ }
- }
- } else {
- try {
- await createBookmarkTool.call({
- data: {
- priority: "want_to_attend",
- relationships: [{ rel: "bookmarks", target: sessionId }],
- },
- });
- } catch { /* */ }
- }
- await loadBookmarks();
- }
+ /* ---------- speakers debounced ---------- */
- // Load data on mount and tab change
- useEffect(() => { loadSchedule(); }, [day, loadSchedule]);
- useEffect(() => { loadBookmarks(); }, [loadBookmarks]);
const speakerTimerRef = useRef | null>(null);
const speakerSeqRef = useRef(0);
+
useEffect(() => {
if (tab !== "speakers") return;
if (speakerTimerRef.current) clearTimeout(speakerTimerRef.current);
@@ -586,55 +347,106 @@ function SummitUI() {
const data = asDict(result.data);
setSpeakers((data.results as Speaker[]) || []);
} catch { /* */ }
- }, 600);
+ }, 250);
return () => { if (speakerTimerRef.current) clearTimeout(speakerTimerRef.current); };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab, speakerQuery, speakerFilter]);
- // Refresh on agent data changes
+ /* ---------- bookmarks: toggle ---------- */
+
+ async function toggleBookmark(sessionId: string) {
+ if (bookmarkedSessionIds.has(sessionId)) {
+ const bk = bookmarks.find((b) => getSessionIdFromBookmark(b) === sessionId);
+ if (bk) {
+ try { await deleteBookmarkTool.call({ bookmark_id: bk.id }); } catch { /* */ }
+ }
+ } else {
+ try {
+ await bookmarkSessionTool.call({ session_id: sessionId });
+ } catch { /* */ }
+ }
+ await loadBookmarks();
+ }
+
+ /* ---------- effects ---------- */
+
+ useEffect(() => { loadSchedule(); }, [day, loadSchedule]);
+ useEffect(() => { loadBookmarks(); }, [loadBookmarks]);
useDataSync(() => {
loadSchedule();
loadBookmarks();
});
- /* ---------- session card ---------- */
+ /* ---------- escape closes modal ---------- */
+
+ useEffect(() => {
+ if (!selectedSession) return;
+ const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setSelectedSession(null); };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [selectedSession]);
- function SessionCard({ session, compact }: { session: Session; compact?: boolean }) {
+ /* ---------- session row ---------- */
+
+ function SessionRow({ session }: { session: Session }) {
const isBookmarked = bookmarkedSessionIds.has(session.id);
+ const sessionType = session.session_type
+ || ((session as unknown as Record).type as string)
+ || "";
+ const isClickable = CLICKABLE_TYPES.has(sessionType);
+ const isKeynote = sessionType === "keynote";
const speakerNames = session.speakers?.map((sp) => sp.name) || session.speaker_names || [];
- // Schedule tool returns "type", search tool returns "session_type"
- const sessionType = session.session_type || (session as Record).type as string || "";
- const isClickable = ["talk", "keynote", "workshop", "sponsor_activity"].includes(sessionType);
+
return (
isClickable && openSessionDetail({ ...session, session_type: sessionType })}
>
{ e.stopPropagation(); toggleBookmark(session.id); }}
- title={isBookmarked ? "Remove bookmark" : "Bookmark session"}
>
- {isBookmarked ? "\u2605" : "\u2606"}
+ {isBookmarked ? : }
-
-
{session.title}
-
-
{sessionType}
- {session.room &&
{session.room} }
- {session.start_time &&
· {session.start_time}-{session.end_time} }
+
+
{session.title}
+
+ {sessionType.replace(/_/g, " ")}
+ {session.room && {session.room} }
+ {session.start_time && (
+
+ {fmtTimeRange(session.start_time, session.end_time)}
+
+ )}
- {!compact && speakerNames.length > 0 && (
-
- {speakerNames.join(", ")}
-
+ {speakerNames.length > 0 && (
+
{speakerNames.join(", ")}
)}
);
}
- /* ---------- session detail modal ---------- */
+ function BreakRow({ session }: { session: Session }) {
+ return (
+
+
+ {session.start_time ? fmtTimeRange(session.start_time, session.end_time) : ""}
+
+ {session.title}
+ {session.room && · {session.room} }
+
+ );
+ }
+
+ /* ---------- modal ---------- */
function SessionDetailModal() {
if (!selectedSession) return null;
@@ -642,129 +454,122 @@ function SummitUI() {
const isBookmarked = bookmarkedSessionIds.has(sess.id);
const speakerNames = sess.speakers?.map((sp) => sp.name) || sess.speaker_names || [];
const speakerCompanies = sess.speakers?.map((sp) => sp.company) || sess.speaker_companies || [];
- const dayLabel = DAYS.find((d) => d.value === sess.day)?.label || sess.day;
+ const dayInfo = DAYS.find((d) => d.value === sess.day);
+ const dayLabel = dayInfo ? `${dayInfo.short} ${dayInfo.date}` : sess.day;
return (
-
setSelectedSession(null)}
- >
-
e.stopPropagation()}
- >
- {/* Header */}
-
-
-
- {sess.session_type}
- {sess.track && sess.track !== "keynote" && sess.track !== "special_events" && (
-
- {sess.track.replace(/_/g, " ")}
-
- )}
-
-
{sess.title}
+
setSelectedSession(null)}>
+
e.stopPropagation()} role="dialog" aria-modal="true">
+
+
+ {sess.session_type.replace(/_/g, " ")}
+ {sess.track && sess.track !== "keynote" && sess.track !== "special_events" && (
+ {sess.track.replace(/_/g, " ")}
+ )}
+
{sess.title}
setSelectedSession(null)}
>
- ×
+
- {/* Time & location */}
-
-
-
When
-
{dayLabel} · {sess.start_time}-{sess.end_time}
-
- {sess.room && (
-
-
Where
-
{sess.room}
+
+
+
+
When
+
+ {dayLabel} · {fmtTimeRange(sess.start_time, sess.end_time)}
+
- )}
-
+ {sess.room && (
+
+ )}
+
- {/* Speakers */}
- {(detailSpeakers.length > 0 || speakerNames.length > 0) && (
-
-
Speakers
- {detailSpeakers.length > 0 ? (
- detailSpeakers.map((sp) => (
-
- {sp.photo_url ? (
-
- ) : (
-
{sp.name.charAt(0)}
- )}
-
-
-
{sp.name}
- {(sp as Record
).linkedin_url && (
- { e.preventDefault(); e.stopPropagation(); synapse.openLink((sp as Record).linkedin_url as string); }}
- >LinkedIn
+ {(detailSpeakers.length > 0 || speakerNames.length > 0) && (
+
+
Speakers
+ {detailSpeakers.length > 0 ? (
+ detailSpeakers.map((sp) => (
+
+ {sp.photo_url ? (
+
+ ) : (
+
{sp.name.charAt(0)}
+ )}
+
+
+
+ {sp.role ? `${sp.role}, ` : ""}{sp.company}
+
+ {sp.bio && (
+
+ {sp.bio.length > 220 ? sp.bio.substring(0, 220) + "…" : sp.bio}
+
)}
-
- {sp.role ? `${sp.role}, ` : ""}{sp.company}
-
- {sp.bio && (
-
- {sp.bio.length > 200 ? sp.bio.substring(0, 200) + "..." : sp.bio}
-
+
+ ))
+ ) : (
+ speakerNames.map((name, i) => (
+
+ {name}
+ {speakerCompanies[i] && (
+
+ {" "}· {speakerCompanies[i]}
+
)}
-
- ))
- ) : (
- speakerNames.map((name, i) => (
-
- {name}
- {speakerCompanies[i] && · {speakerCompanies[i]} }
-
- ))
- )}
-
- )}
+ ))
+ )}
+
+ )}
- {/* Description */}
- {detailLoading ? (
-
Loading details...
- ) : sess.description ? (
-
-
About
-
- {sess.description}
+ {detailLoading ? (
+
Loading details…
+ ) : sess.description ? (
+
+
About
+
{sess.description}
-
- ) : null}
+ ) : null}
+
- {/* Actions */}
-
+
toggleBookmark(sess.id)}
>
- {isBookmarked ? "Remove Bookmark" : "Bookmark Session"}
+ {isBookmarked ? "Remove bookmark" : "Bookmark session"}
{sess.sched_url && (
sess.sched_url && window.parent.postMessage(
- { jsonrpc: "2.0", id: "lnk", method: "ui/open-link", params: { url: sess.sched_url } }, "*"
- )}
- >
- Sched →
-
+ type="button"
+ className="summit-btn summit-btn--ghost"
+ onClick={() => sess.sched_url && synapse.openLink(sess.sched_url)}
+ >Sched ↗
)}
@@ -777,29 +582,62 @@ function SummitUI() {
function ScheduleView() {
return (
<>
-
+
{DAYS.map((d) => (
- setDay(d.value)}>
- {d.label}
+ setDay(d.value)}
+ >
+ {d.short}
+ {d.date}
))}
- {scheduleLabel && (
-
{scheduleLabel}
- )}
- {scheduleTool.isPending ? (
-
Loading schedule...
+ {scheduleTool.isPending && schedule.length === 0 ? (
+
) : schedule.length === 0 ? (
-
No sessions found for this day.
+
+
∅
+
No sessions
+
Nothing on the schedule for this day yet.
+
) : (
- schedule.map((slot) => (
-
-
{slot.time}
- {slot.sessions.map((sess) => (
-
- ))}
-
- ))
+ schedule.map((slot) => {
+ const typeOf = (s: Session) => s.session_type || ((s as unknown as Record
).type as string) || "";
+ const breaks = slot.sessions.filter((s) => BREAK_TYPES.has(typeOf(s)));
+ const real = slot.sessions.filter((s) => !BREAK_TYPES.has(typeOf(s)));
+
+ // If the slot is only breaks, render them as a single divider line.
+ if (real.length === 0 && breaks.length > 0) {
+ return (
+
+ {breaks.map((b) => )}
+
+ );
+ }
+
+ return (
+
+
+ {slot.time}
+
+
+ {real.length} {real.length === 1 ? "session" : "sessions"}
+
+
+ {real.map((sess) =>
)}
+ {breaks.map((b) =>
)}
+
+ );
+ })
+ )}
+ {scheduleLabel && (
+
+ {scheduleLabel}
+
)}
>
);
@@ -814,62 +652,82 @@ function SummitUI() {
return { bk, session, sessionId };
});
- // Group by day if session details available, otherwise show flat list
const withDay = enriched.filter((e) => e.session?.day);
const withoutDay = enriched.filter((e) => !e.session?.day);
-
const byDay = DAYS.map((d) => ({
...d,
items: withDay.filter((e) => e.session!.day === d.value),
})).filter((d) => d.items.length > 0);
- const renderBookmark = ({ bk, session, sessionId }: typeof enriched[0]) => (
-
-
sessionId && toggleBookmark(sessionId)}
- title="Remove bookmark"
- >
- {"\u2605"}
-
-
-
-
-
{session?.title || sessionId || "Bookmarked session"}
+ const renderRow = ({ bk, session, sessionId }: typeof enriched[0]) => (
+
session && openSessionDetail(session)}
+ >
+
+
+
+ {session?.title || sessionId || "(missing session)"}
{session && (
-
- {session.start_time}-{session.end_time} · {session.room}
+
+ {session.start_time && fmtTimeRange(session.start_time, session.end_time)}
+ {session.room && ` · ${session.room}`}
)}
+
{ e.stopPropagation(); sessionId && toggleBookmark(sessionId); }}
+ >
+
+
);
+ if (listBookmarksTool.isPending && bookmarks.length === 0) {
+ return
;
+ }
+ if (bookmarks.length === 0) {
+ return (
+
+
★
+
No bookmarks yet
+
+ Tap
+
+ next to any session to save it here.
+
+
+ );
+ }
+
return (
<>
- {listBookmarksTool.isPending ? (
-
Loading bookmarks...
- ) : bookmarks.length === 0 ? (
-
- No sessions bookmarked yet.
- Use the schedule tab to bookmark sessions.
+ {byDay.map((d) => (
+
+
+ {d.short} · {d.date}
+ {d.items.length}
+
+ {d.items.map(renderRow)}
- ) : (
- <>
- {byDay.map((d) => (
-
-
{d.label}
- {d.items.map(renderBookmark)}
-
- ))}
- {withoutDay.length > 0 && (
-
- {byDay.length > 0 &&
Other
}
- {withoutDay.map(renderBookmark)}
+ ))}
+ {withoutDay.length > 0 && (
+
+ {byDay.length > 0 && (
+
+ Other
+ {withoutDay.length}
)}
- >
+ {withoutDay.map(renderRow)}
+
)}
>
);
@@ -878,68 +736,69 @@ function SummitUI() {
/* ---------- speakers view ---------- */
function SpeakersView() {
- const isExpanded = (id: string) => expandedSpeaker === id;
-
return (
<>
- {/* Search + filter */}
-
+
+
setSpeakerQuery(e.target.value)}
/>
-
- setSpeakerFilter("all")}
- >All Speakers
- setSpeakerFilter("keynote")}
- >Keynotes Only
-
+ {speakerQuery && (
+
setSpeakerQuery("")} aria-label="Clear search">
+
+
+ )}
- {speakersTool.isPending ? (
-
Loading speakers...
+
+ setSpeakerFilter("all")}
+ >All
+ setSpeakerFilter("keynote")}
+ >Keynotes
+
+
+ {speakersTool.isPending && speakers.length === 0 ? (
+
) : speakers.length === 0 ? (
-
No speakers found.
+
+
⌖
+
No speakers found
+
Try a different search or clear the filter.
+
) : (
<>
-
- {speakers.length} speakers
-
+
{speakers.length} speakers
{speakers.map((sp) => (
setExpandedSpeaker(isExpanded(sp.id) ? null : sp.id)}
+ className="summit-spk"
+ onClick={() => setExpandedSpeaker(expandedSpeaker === sp.id ? null : sp.id)}
>
-
- {/* Photo */}
+
{sp.photo_url ? (
-
+
) : (
-
- {sp.name.charAt(0)}
-
+
{sp.name.charAt(0)}
)}
-
-
-
-
{sp.name}
+
+
-
+
{sp.role ? `${sp.role}, ` : ""}{sp.company}
-
- {/* Topics */}
{sp.topics && sp.topics.length > 0 && (
-
- {sp.topics.map((topic, i) => (
-
- {topic}
-
+
+ {sp.topics.slice(0, 4).map((t, i) => (
+ {t}
))}
+ {sp.topics.length > 4 && (
+ +{sp.topics.length - 4}
+ )}
)}
-
- {/* Expanded: bio + sessions */}
- {isExpanded(sp.id) && (
-
- {sp.bio && (
-
- {sp.bio}
-
- )}
+ {expandedSpeaker === sp.id && (
+
+ {sp.bio &&
{sp.bio}
}
{sp.sessions && sp.sessions.length > 0 && (
-
Sessions
- {sp.sessions.map((sess) => (
-
- {DAYS.find(d => d.value === sess.day)?.label || sess.day} {sess.start_time} — {sess.title}
-
- ))}
+
Sessions
+
+ {sp.sessions.map((sess) => {
+ const dInfo = DAYS.find((d) => d.value === sess.day);
+ return (
+
+
+ {dInfo ? `${dInfo.short} ${sess.start_time}` : sess.start_time}
+
+ {sess.title}
+
+ );
+ })}
+
)}
@@ -997,59 +857,97 @@ function SummitUI() {
function SearchView() {
return (
<>
-
+
+
setSearchQuery(e.target.value)}
autoFocus
/>
+ {searchQuery && (
+ setSearchQuery("")} aria-label="Clear search">
+
+
+ )}
+
+ {!searchQuery.trim() && (
+ <>
+
+ {SEARCH_SUGGESTIONS.map((q) => (
+ setSearchQuery(q)}>
+ {q}
+
+ ))}
+
+
+
⌕
+
Search the conference
+
Find sessions by topic, speaker, company, or keyword.
+
+ >
+ )}
+
{searching && (
-
Searching...
+
)}
{!searching && searchQuery.trim() && searchTotal > 0 && (
-
- {searchTotal} results
-
- )}
- {!searching && searchQuery.trim() && searchTotal === 0 && searchResults.length === 0 && (
-
No sessions found for "{searchQuery}"
+
{searchTotal} results
)}
- {!searchQuery.trim() && (
-
Type to search across sessions, speakers, and topics
+ {!searching && searchQuery.trim() && searchResults.length === 0 && (
+
+
∅
+
No matches for "{searchQuery}"
+
Try a broader term or check spelling.
+
)}
- {searchResults.map((sess) => (
-
- ))}
+ {!searching && searchResults.map((sess) =>
)}
>
);
}
- /* ---------- render ---------- */
+ /* ---------- nav ---------- */
+
+ const tabs: { key: Tab; label: string; count?: number }[] = useMemo(() => [
+ { key: "schedule", label: "Schedule" },
+ { key: "bookmarks", label: "Bookmarks", count: bookmarks.length },
+ { key: "speakers", label: "Speakers" },
+ { key: "search", label: "Search" },
+ ], [bookmarks.length]);
return (
<>
-
-
- setTab("schedule")}>Schedule
- setTab("bookmarks")}>
- Bookmarks{bookmarks.length > 0 ? ` (${bookmarks.length})` : ""}
-
- setTab("speakers")}>Speakers
- setTab("search")}>Search
-
+
+
+
+ {tabs.map((t) => (
+ setTab(t.key)}
+ >
+ {t.label}
+ {t.count !== undefined && t.count > 0 && (
+ {t.count}
+ )}
+
+ ))}
+
- {tab === "schedule" && ScheduleView()}
- {tab === "bookmarks" && BookmarksView()}
- {tab === "speakers" && SpeakersView()}
- {tab === "search" && SearchView()}
+ {tab === "schedule" && }
+ {tab === "bookmarks" && }
+ {tab === "speakers" && }
+ {tab === "search" && }
- {SessionDetailModal()}
+ {SessionDetailModal()}
+
>
);
@@ -1057,7 +955,7 @@ function SummitUI() {
export function App() {
return (
-
+
);
diff --git a/ui/src/styles.ts b/ui/src/styles.ts
new file mode 100644
index 0000000..32f2929
--- /dev/null
+++ b/ui/src/styles.ts
@@ -0,0 +1,780 @@
+/**
+ * Visual system for the MCP Dev Summit companion.
+ *
+ * Self-contained palette: drives off `data-theme` on :root rather than
+ * relying on host tokens to fill every variable. The host's
+ * `--color-text-accent`, when present, overrides our accent fallback —
+ * so the widget picks up brand color without breaking when other tokens
+ * are missing or inconsistent.
+ */
+export const SUMMIT_CSS = `
+:root[data-theme="light"] {
+ --summit-surface: #ffffff;
+ --summit-surface-2: #f8f9fb;
+ --summit-surface-3: #eef0f4;
+ --summit-ink: #0c111c;
+ --summit-ink-2: #4a5468;
+ --summit-ink-3: #8b95a8;
+ --summit-rule: #e5e8ee;
+ --summit-rule-soft: #eef0f4;
+ --summit-accent: var(--color-text-accent, #4f46e5);
+ --summit-accent-ink: #ffffff;
+ --summit-accent-soft: color-mix(in srgb, var(--summit-accent) 10%, transparent);
+ --summit-accent-line: color-mix(in srgb, var(--summit-accent) 28%, transparent);
+ --summit-star: #c2820a;
+ --summit-danger: #dc2626;
+ --summit-success: #15803d;
+ --summit-warning: #b45309;
+ --summit-shadow: 0 1px 2px rgba(12, 17, 28, 0.04), 0 8px 24px rgba(12, 17, 28, 0.06);
+}
+:root[data-theme="dark"] {
+ --summit-surface: #0b101a;
+ --summit-surface-2: #131a28;
+ --summit-surface-3: #1c2536;
+ --summit-ink: #e7ecf3;
+ --summit-ink-2: #98a3b8;
+ --summit-ink-3: #5e6b82;
+ --summit-rule: #1e2840;
+ --summit-rule-soft: #161e2e;
+ --summit-accent: var(--color-text-accent, #818cf8);
+ --summit-accent-ink: #0b101a;
+ --summit-accent-soft: color-mix(in srgb, var(--summit-accent) 14%, transparent);
+ --summit-accent-line: color-mix(in srgb, var(--summit-accent) 35%, transparent);
+ --summit-star: #facc15;
+ --summit-danger: #f87171;
+ --summit-success: #4ade80;
+ --summit-warning: #f59e0b;
+ --summit-shadow: 0 1px 2px rgba(0,0,0,0.4), 0 8px 24px rgba(0,0,0,0.4);
+}
+
+/* ---------- Reset within widget ---------- */
+.summit-root,
+.summit-root * {
+ box-sizing: border-box;
+}
+.summit-root button {
+ font: inherit;
+}
+
+/* ---------- Layout ---------- */
+.summit-root {
+ background: var(--summit-surface);
+ color: var(--summit-ink);
+ font-family: var(--font-sans, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter var", "Segoe UI", sans-serif);
+ font-size: 13px;
+ line-height: 1.5;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+.summit-shell {
+ padding: 14px 14px 32px;
+ max-width: 720px;
+ margin: 0 auto;
+}
+
+/* ---------- Tabs ---------- */
+.summit-nav {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--summit-rule);
+ margin-bottom: 14px;
+ position: sticky;
+ top: 0;
+ background: var(--summit-surface);
+ z-index: 5;
+}
+.summit-nav__tab {
+ appearance: none;
+ background: transparent;
+ border: none;
+ border-bottom: 2px solid transparent;
+ padding: 10px 4px;
+ margin-right: 18px;
+ color: var(--summit-ink-2);
+ font-size: 12.5px;
+ font-weight: 500;
+ letter-spacing: 0.01em;
+ cursor: pointer;
+ transition: color 120ms, border-color 120ms;
+}
+.summit-nav__tab:hover { color: var(--summit-ink); }
+.summit-nav__tab[aria-selected="true"] {
+ color: var(--summit-ink);
+ border-bottom-color: var(--summit-accent);
+ font-weight: 600;
+}
+.summit-nav__count {
+ margin-left: 4px;
+ font-variant-numeric: tabular-nums;
+ color: var(--summit-ink-3);
+ font-weight: 400;
+}
+.summit-nav__tab[aria-selected="true"] .summit-nav__count { color: var(--summit-accent); }
+
+/* ---------- Day picker (segmented) ---------- */
+.summit-days {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 14px;
+}
+.summit-days__btn {
+ flex: 1;
+ appearance: none;
+ border: 1px solid var(--summit-rule);
+ background: var(--summit-surface);
+ color: var(--summit-ink-2);
+ padding: 9px 8px;
+ border-radius: 8px;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 120ms;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1px;
+ font-variant-numeric: tabular-nums;
+}
+.summit-days__btn:hover {
+ border-color: var(--summit-accent-line);
+ color: var(--summit-ink);
+}
+.summit-days__btn[aria-pressed="true"] {
+ background: var(--summit-accent-soft);
+ color: var(--summit-accent);
+ border-color: var(--summit-accent-line);
+ box-shadow: inset 0 0 0 1px var(--summit-accent-line);
+}
+.summit-days__btn[aria-pressed="true"] .summit-days__btn--day { opacity: 0.8; }
+.summit-days__btn--day { font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.7; }
+.summit-days__btn--date { font-size: 13px; font-weight: 600; }
+
+/* ---------- Time slot ---------- */
+.summit-slot {
+ margin-bottom: 18px;
+}
+.summit-slot__head {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ padding: 6px 0 8px;
+ border-bottom: 1px solid var(--summit-rule);
+ margin-bottom: 4px;
+}
+.summit-slot__time {
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
+ font-size: 11.5px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ color: var(--summit-ink);
+ font-variant-numeric: tabular-nums;
+}
+.summit-slot__rule {
+ flex: 1;
+ height: 1px;
+ background: transparent;
+}
+.summit-slot__count {
+ font-size: 11px;
+ color: var(--summit-ink-3);
+ font-variant-numeric: tabular-nums;
+}
+
+/* ---------- Session row ---------- */
+.summit-row {
+ display: grid;
+ grid-template-columns: 28px 1fr;
+ gap: 10px;
+ padding: 11px 0;
+ border-bottom: 1px solid var(--summit-rule-soft);
+ cursor: default;
+ position: relative;
+}
+.summit-row:last-child { border-bottom: none; }
+.summit-row[data-clickable="true"] { cursor: pointer; }
+.summit-row[data-clickable="true"]:hover { background: var(--summit-surface-2); margin: 0 -8px; padding-left: 8px; padding-right: 8px; }
+.summit-row[data-bookmarked="true"]::before {
+ content: "";
+ position: absolute;
+ left: -14px;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background: var(--summit-star);
+}
+.summit-row[data-keynote="true"]::before {
+ content: "";
+ position: absolute;
+ left: -14px;
+ top: 0;
+ bottom: 0;
+ width: 3px;
+ background: var(--summit-accent);
+}
+.summit-row__title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--summit-ink);
+ line-height: 1.35;
+ margin: 0;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+.summit-row__meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 10px;
+ margin-top: 4px;
+ font-size: 12px;
+ color: var(--summit-ink-2);
+ align-items: baseline;
+}
+.summit-row__meta-time {
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
+ color: var(--summit-ink-3);
+ font-size: 11.5px;
+ font-variant-numeric: tabular-nums;
+}
+.summit-row__meta-room { color: var(--summit-ink-2); }
+.summit-row__speakers {
+ font-size: 12px;
+ color: var(--summit-ink-3);
+ margin-top: 2px;
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+/* ---------- Bookmark button ---------- */
+.summit-bk {
+ appearance: none;
+ background: transparent;
+ border: 1px solid var(--summit-rule);
+ border-radius: 999px;
+ width: 28px;
+ height: 28px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ color: var(--summit-ink-3);
+ transition: all 120ms;
+ padding: 0;
+ flex-shrink: 0;
+ margin-top: 1px;
+}
+.summit-bk:hover {
+ color: var(--summit-ink);
+ border-color: var(--summit-ink-2);
+}
+.summit-bk[aria-pressed="true"] {
+ background: var(--summit-star);
+ border-color: var(--summit-star);
+ color: #1a1306;
+}
+.summit-bk svg { width: 14px; height: 14px; }
+
+/* ---------- Break row ---------- */
+.summit-break {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 0;
+ color: var(--summit-ink-3);
+ font-size: 12px;
+}
+.summit-break::before {
+ content: "";
+ flex: 0 0 18px;
+ height: 1px;
+ background: var(--summit-rule);
+}
+.summit-break::after {
+ content: "";
+ flex: 1;
+ height: 1px;
+ background: var(--summit-rule);
+}
+.summit-break__time {
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
+ font-variant-numeric: tabular-nums;
+ color: var(--summit-ink-2);
+ font-weight: 500;
+}
+
+/* ---------- Badges ---------- */
+.summit-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 10.5px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ padding: 2px 6px;
+ border-radius: 4px;
+ background: var(--summit-surface-3);
+ color: var(--summit-ink-2);
+}
+.summit-badge--keynote { background: var(--summit-accent-soft); color: var(--summit-accent); }
+.summit-badge--workshop { background: color-mix(in srgb, var(--summit-success) 14%, transparent); color: var(--summit-success); }
+.summit-badge--social { background: color-mix(in srgb, #ec4899 14%, transparent); color: #ec4899; }
+.summit-badge--sponsor_activity { background: color-mix(in srgb, var(--summit-warning) 14%, transparent); color: var(--summit-warning); }
+.summit-badge--track {
+ background: transparent;
+ color: var(--summit-ink-3);
+ border: 1px solid var(--summit-rule);
+ font-weight: 500;
+}
+
+/* ---------- Search ---------- */
+.summit-search {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ border: 1px solid var(--summit-rule);
+ border-radius: 10px;
+ padding: 0 12px;
+ background: var(--summit-surface);
+ margin-bottom: 14px;
+ transition: border-color 120ms, box-shadow 120ms;
+}
+.summit-search:focus-within {
+ border-color: var(--summit-accent);
+ box-shadow: 0 0 0 3px var(--summit-accent-soft);
+}
+.summit-search__icon {
+ color: var(--summit-ink-3);
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+}
+.summit-search__input {
+ flex: 1;
+ appearance: none;
+ border: none;
+ background: transparent;
+ outline: none;
+ padding: 11px 0;
+ font-size: 14px;
+ color: var(--summit-ink);
+ min-width: 0;
+}
+.summit-search__input::placeholder { color: var(--summit-ink-3); }
+.summit-search__clear {
+ appearance: none;
+ background: transparent;
+ border: none;
+ color: var(--summit-ink-3);
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+}
+.summit-search__clear:hover { color: var(--summit-ink); }
+
+.summit-suggestions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 16px;
+}
+.summit-suggest {
+ appearance: none;
+ background: var(--summit-surface-2);
+ border: 1px solid var(--summit-rule);
+ color: var(--summit-ink-2);
+ font-size: 12px;
+ padding: 5px 10px;
+ border-radius: 999px;
+ cursor: pointer;
+ transition: all 120ms;
+}
+.summit-suggest:hover {
+ background: var(--summit-surface-3);
+ color: var(--summit-ink);
+ border-color: var(--summit-ink-3);
+}
+
+/* ---------- Filter pills (speakers) ---------- */
+.summit-filters {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 12px;
+}
+.summit-filter {
+ appearance: none;
+ border: 1px solid var(--summit-rule);
+ background: transparent;
+ color: var(--summit-ink-2);
+ font-size: 12px;
+ padding: 6px 12px;
+ border-radius: 999px;
+ cursor: pointer;
+ font-weight: 500;
+}
+.summit-filter[aria-pressed="true"] {
+ background: var(--summit-accent-soft);
+ color: var(--summit-accent);
+ border-color: var(--summit-accent-line);
+}
+
+/* ---------- Speaker cards ---------- */
+.summit-speakers__count {
+ font-size: 11.5px;
+ color: var(--summit-ink-3);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-bottom: 8px;
+ font-variant-numeric: tabular-nums;
+}
+.summit-spk {
+ padding: 12px 0;
+ border-bottom: 1px solid var(--summit-rule-soft);
+ cursor: pointer;
+}
+.summit-spk:last-child { border-bottom: none; }
+.summit-spk__head {
+ display: grid;
+ grid-template-columns: 44px 1fr;
+ gap: 12px;
+ align-items: flex-start;
+}
+.summit-spk__avatar {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ object-fit: cover;
+ background: var(--summit-surface-3);
+}
+.summit-spk__initial {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--summit-accent-soft);
+ color: var(--summit-accent);
+ font-size: 16px;
+ font-weight: 600;
+}
+.summit-spk__name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--summit-ink);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+.summit-spk__role {
+ font-size: 12px;
+ color: var(--summit-ink-2);
+ margin-top: 2px;
+}
+.summit-spk__topics {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 6px;
+}
+.summit-spk__topic {
+ font-size: 10.5px;
+ color: var(--summit-ink-3);
+ background: var(--summit-surface-2);
+ padding: 1px 7px;
+ border-radius: 3px;
+}
+.summit-spk__expand {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid var(--summit-rule-soft);
+ font-size: 12.5px;
+ line-height: 1.5;
+ color: var(--summit-ink-2);
+}
+.summit-spk__bio { margin-bottom: 10px; }
+.summit-spk__sess-list { display: flex; flex-direction: column; gap: 3px; }
+.summit-spk__sess-item {
+ font-size: 12px;
+ color: var(--summit-ink-2);
+ display: flex;
+ gap: 8px;
+}
+.summit-spk__sess-time {
+ font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
+ color: var(--summit-ink-3);
+ font-variant-numeric: tabular-nums;
+ flex-shrink: 0;
+ min-width: 86px;
+}
+
+/* ---------- Modal ---------- */
+.summit-modal__overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(8, 11, 18, 0.55);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 50;
+ padding: 16px;
+ animation: summitFadeIn 140ms ease-out;
+}
+@keyframes summitFadeIn { from { opacity: 0; } to { opacity: 1; } }
+@keyframes summitSlideIn { from { transform: translateY(8px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
+.summit-modal {
+ background: var(--summit-surface);
+ border: 1px solid var(--summit-rule);
+ border-radius: 14px;
+ width: 100%;
+ max-width: 520px;
+ max-height: 80vh;
+ overflow-y: auto;
+ color: var(--summit-ink);
+ box-shadow: var(--summit-shadow);
+ animation: summitSlideIn 180ms ease-out;
+}
+.summit-modal__head {
+ padding: 18px 20px 12px;
+ border-bottom: 1px solid var(--summit-rule);
+ position: sticky;
+ top: 0;
+ background: var(--summit-surface);
+}
+.summit-modal__close {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ width: 30px;
+ height: 30px;
+ border-radius: 999px;
+ border: none;
+ background: var(--summit-surface-2);
+ color: var(--summit-ink-2);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.summit-modal__close:hover { background: var(--summit-surface-3); color: var(--summit-ink); }
+.summit-modal__eyebrow {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 8px;
+ flex-wrap: wrap;
+}
+.summit-modal__title {
+ font-size: 18px;
+ font-weight: 600;
+ line-height: 1.25;
+ margin: 0;
+ color: var(--summit-ink);
+ padding-right: 32px;
+}
+.summit-modal__body { padding: 16px 20px; }
+.summit-modal__row {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+ margin-bottom: 14px;
+}
+.summit-modal__cell { flex: 0 0 auto; }
+.summit-label {
+ font-size: 10.5px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--summit-ink-3);
+ margin-bottom: 3px;
+ font-weight: 600;
+}
+.summit-modal__when {
+ font-size: 13px;
+ color: var(--summit-ink);
+ font-variant-numeric: tabular-nums;
+}
+.summit-modal__section { margin-bottom: 16px; }
+.summit-modal__section:last-child { margin-bottom: 0; }
+.summit-modal__desc {
+ font-size: 13.5px;
+ line-height: 1.6;
+ color: var(--summit-ink);
+ white-space: pre-wrap;
+}
+.summit-modal__speaker {
+ display: grid;
+ grid-template-columns: 36px 1fr;
+ gap: 10px;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--summit-rule-soft);
+}
+.summit-modal__speaker:last-child { border-bottom: none; }
+.summit-modal__speaker img,
+.summit-modal__speaker .summit-spk__initial {
+ width: 36px;
+ height: 36px;
+ font-size: 14px;
+}
+.summit-modal__sp-name {
+ font-size: 13.5px;
+ font-weight: 600;
+ color: var(--summit-ink);
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+.summit-modal__sp-role {
+ font-size: 12px;
+ color: var(--summit-ink-2);
+ margin-top: 1px;
+}
+.summit-modal__sp-bio {
+ font-size: 12.5px;
+ color: var(--summit-ink-2);
+ margin-top: 6px;
+ line-height: 1.5;
+}
+.summit-modal__actions {
+ display: flex;
+ gap: 8px;
+ padding: 14px 20px 18px;
+ border-top: 1px solid var(--summit-rule);
+ position: sticky;
+ bottom: 0;
+ background: var(--summit-surface);
+}
+.summit-btn {
+ appearance: none;
+ border: none;
+ border-radius: 8px;
+ padding: 10px 14px;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 120ms;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+}
+.summit-btn--primary {
+ background: var(--summit-ink);
+ color: var(--summit-surface);
+ flex: 1;
+}
+.summit-btn--primary:hover { opacity: 0.9; }
+.summit-btn--ghost {
+ background: transparent;
+ color: var(--summit-ink);
+ border: 1px solid var(--summit-rule);
+}
+.summit-btn--ghost:hover { background: var(--summit-surface-2); }
+.summit-btn--unbookmark {
+ background: var(--summit-surface-2);
+ color: var(--summit-ink);
+ border: 1px solid var(--summit-rule);
+ flex: 1;
+}
+
+/* ---------- Bookmark item ---------- */
+.summit-bk-row {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 8px;
+ padding: 11px 0;
+ border-bottom: 1px solid var(--summit-rule-soft);
+ cursor: pointer;
+ align-items: center;
+}
+.summit-bk-row:last-child { border-bottom: none; }
+.summit-bk-row:hover { background: var(--summit-surface-2); margin: 0 -8px; padding-left: 8px; padding-right: 8px; }
+.summit-bk-row__title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--summit-ink);
+ line-height: 1.35;
+}
+.summit-bk-row__meta {
+ font-size: 12px;
+ color: var(--summit-ink-3);
+ margin-top: 2px;
+ font-variant-numeric: tabular-nums;
+}
+.summit-bk-row__priority {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--summit-ink-3);
+ margin-right: 6px;
+ vertical-align: middle;
+}
+.summit-bk-row__priority--must { background: var(--summit-danger); }
+.summit-bk-row__priority--want { background: var(--summit-star); }
+
+/* ---------- Empty / loading ---------- */
+.summit-empty {
+ text-align: center;
+ padding: 48px 16px;
+ color: var(--summit-ink-3);
+}
+.summit-empty__icon { font-size: 32px; opacity: 0.4; margin-bottom: 10px; line-height: 1; }
+.summit-empty__title { font-size: 14px; font-weight: 600; color: var(--summit-ink-2); margin-bottom: 4px; }
+.summit-empty__hint { font-size: 12.5px; line-height: 1.5; max-width: 280px; margin: 0 auto; }
+
+.summit-skeleton {
+ background: linear-gradient(90deg, var(--summit-surface-2) 0%, var(--summit-surface-3) 50%, var(--summit-surface-2) 100%);
+ background-size: 200% 100%;
+ animation: summitShimmer 1.4s linear infinite;
+ border-radius: 4px;
+}
+.summit-skeleton-row {
+ padding: 12px 0;
+ border-bottom: 1px solid var(--summit-rule-soft);
+}
+.summit-skeleton-row__line {
+ height: 14px;
+ background: var(--summit-surface-2);
+ border-radius: 4px;
+ margin-bottom: 6px;
+}
+.summit-skeleton-row__line--short { width: 40%; height: 11px; }
+@keyframes summitShimmer {
+ from { background-position: 0% 0; }
+ to { background-position: -200% 0; }
+}
+
+/* ---------- Day section header (in bookmarks) ---------- */
+.summit-day-head {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ font-weight: 600;
+ color: var(--summit-ink);
+ padding: 16px 0 6px;
+ border-bottom: 1px solid var(--summit-rule);
+ margin-bottom: 4px;
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+}
+.summit-day-head:first-child { padding-top: 0; }
+.summit-day-head__count { color: var(--summit-ink-3); font-weight: 400; }
+
+/* ---------- Link ---------- */
+.summit-link,
+.summit-link:visited,
+.summit-link:active {
+ color: var(--summit-accent);
+ text-decoration: none;
+ font-size: 12px;
+ cursor: pointer;
+}
+.summit-link:hover { text-decoration: underline; }
+`;
diff --git a/uv.lock b/uv.lock
index e13b6cf..a8addf2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -539,7 +539,7 @@ wheels = [
[[package]]
name = "mcp-dev-summit"
-version = "0.4.0"
+version = "0.6.0"
source = { editable = "." }
dependencies = [
{ name = "python-dotenv" },