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 ( - """ - - -

Loading speakers...

- -\n" + f"\n" + """ """ @@ -264,53 +249,67 @@ def session_widget_ui() -> str: """Session search results widget using Synapse.connect() for MCP Apps protocol.""" synapse_js = _SYNAPSE_JS return ( - '\n' + '\n' f"\n" "\n" - '

Loading sessions...

\n' + '

Loading sessions…

\n' f"\n" + f"\n" """ """ @@ -335,134 +334,196 @@ def summit_ui() -> str: @mcp.resource("ui://mcp-dev-summit/speaker/{speaker_id}") def speaker_card_ui(speaker_id: str) -> str: """A speaker profile card — rendered inline when a speaker is looked up.""" + import html as html_mod + try: sp = upjack_app.get_entity("speaker", speaker_id) except Exception: - return "

Speaker not found

" + return _wrap_widget('

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'{t}' for t in topics) - - sessions_html = "".join( - f'
' - f"{s.get('day', '')[-5:]} {s.get('start_time', '')} — {s.get('title', '')}
" - for s in sessions + avatar = ( + f'{name}' + if photo + else f'
{name[:1] or "?"}
' ) - links_html = "" + subtitle = f"{role + ', ' if role else ''}{company}" + + name_row = f"{name}" if linkedin: - links_html += ( - f'LinkedIn ↗' + name_row += ( + f'LinkedIn ↗' ) - synapse_js = _SYNAPSE_JS - - return ( - f""" - -
-
- {"" + name + "" if photo else "
" + name[0] + "
"} -
-
{name}
-
{(role + ", ") if role else ""}{company}
-
-
- {f'
{bio}
' if bio else ""} - {f'
Topics
{topics_html}
' if topics else ""} - {f'
Sessions
{sessions_html}' if sessions_html else ""} - {f'' if links_html else ""} -
- - -""" + topics_html = "" + if topics: + chips = "".join(f'{html_mod.escape(t)}' for t in topics) + topics_html = f'
Topics
{chips}
' + + sessions_html = "" + if sessions: + rows = "".join( + f'
' + f"{html_mod.escape(s.get('day', '')[-5:])} {html_mod.escape(s.get('start_time', ''))}" + f"{html_mod.escape(s.get('title', ''))}
" + for s in sessions + ) + sessions_html = f'
Sessions
{rows}' + + body = ( + '
' + f"{avatar}" + '
' + f'
{name_row}
' + f"{f'
{subtitle}
' if subtitle else ''}" + f"{f'
{bio}
' if bio else ''}" + f"{topics_html}" + f"{sessions_html}" + "
" ) + return _wrap_widget(body, "speaker-card") + # Shared state for baked-in widget data. The tool stores its result here, # and the resource reads it when Claude Desktop fetches the widget HTML. _last_widget_data: dict[str, Any] = {} _WIDGET_CSS = """\ -body{padding:16px;background:transparent;color:var(--color-text-primary,#e2e8f0); - font-family:var(--font-sans,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif)} -.card{max-width:420px;margin-bottom:12px} -.header{display:flex;gap:12px;margin-bottom:10px} -.photo{width:52px;height:52px;border-radius:50%;object-fit:cover;flex-shrink:0} -.initial{width:52px;height:52px;border-radius:50%; - background:var(--color-background-tertiary,#1e293b); - display:flex;align-items:center;justify-content:center;font-size:18px; - color:var(--color-text-accent,#818cf8);flex-shrink:0} -.name{font-size:15px;font-weight:var(--font-weight-semibold,600);color:var(--color-text-primary,#e2e8f0)} -.role{font-size:var(--font-text-xs-size,12px);color:var(--color-text-secondary,#94a3b8);margin-top:2px} -.bio{font-size:var(--font-text-xs-size,12px);line-height:1.5;color:var(--color-text-secondary,#94a3b8);margin:10px 0} -.tags{display:flex;flex-wrap:wrap;gap:3px;margin:6px 0} -.tag{display:inline-block;padding:1px 6px;border-radius:var(--border-radius-xs,4px); - background:var(--color-background-tertiary,#1e293b); - color:var(--color-text-accent,#818cf8);font-size:10px; - border:var(--border-width-regular,1px) solid var(--color-border-primary,#334155)} -.label{font-size:9px;text-transform:uppercase;letter-spacing:0.5px;color:var(--color-text-tertiary,#64748b);margin:10px 0 3px} -.sess{font-size:var(--font-text-xs-size,12px);color:var(--color-text-secondary,#94a3b8);padding:2px 0} -.link,.link:visited,.link:active{color:var(--color-text-accent,#818cf8);text-decoration:none;font-size:var(--font-text-xs-size,12px);display:inline-block;margin-top:6px} +:root[data-theme="light"]{ + --w-ink:#0c111c; --w-ink-2:#4a5468; --w-ink-3:#8b95a8; + --w-rule:#e5e8ee; --w-rule-soft:#eef0f4; + --w-surface-2:#f8f9fb; --w-surface-3:#eef0f4; + --w-accent:var(--color-text-accent,#4f46e5); + --w-accent-soft:color-mix(in srgb, var(--w-accent) 10%, transparent); + --w-accent-line:color-mix(in srgb, var(--w-accent) 28%, transparent); +} +:root[data-theme="dark"]{ + --w-ink:#e7ecf3; --w-ink-2:#98a3b8; --w-ink-3:#5e6b82; + --w-rule:#1e2840; --w-rule-soft:#161e2e; + --w-surface-2:#131a28; --w-surface-3:#1c2536; + --w-accent:var(--color-text-accent,#818cf8); + --w-accent-soft:color-mix(in srgb, var(--w-accent) 14%, transparent); + --w-accent-line:color-mix(in srgb, var(--w-accent) 35%, transparent); +} +*{box-sizing:border-box} +body{padding:12px;background:transparent;color:var(--w-ink); + font-family:var(--font-sans,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif); + font-size:13px;line-height:1.5; + -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility} +.meta{font-size:11px;color:var(--w-ink-3);text-transform:uppercase;letter-spacing:0.08em; + font-weight:600;margin-bottom:10px;font-variant-numeric:tabular-nums} +.empty{color:var(--w-ink-3);font-size:13px;padding:8px 0} +.more{font-size:11px;color:var(--w-ink-3);margin-top:8px;font-style:italic} + +/* ---------- speaker rows ---------- */ +.spk-row{padding:10px 0;border-bottom:1px solid var(--w-rule-soft)} +.spk-row:last-child{border-bottom:none;padding-bottom:0} +.spk-row:first-child{padding-top:0} +.spk-head{display:grid;grid-template-columns:40px 1fr;gap:10px;align-items:flex-start} +.photo{width:40px;height:40px;border-radius:50%;object-fit:cover;flex-shrink:0;background:var(--w-surface-3)} +.initial{width:40px;height:40px;border-radius:50%;flex-shrink:0; + background:var(--w-accent-soft);color:var(--w-accent); + display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:600} +.name{font-size:14px;font-weight:600;color:var(--w-ink); + display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;line-height:1.3} +.role{font-size:12px;color:var(--w-ink-2);margin-top:1px;line-height:1.4} +.bio{font-size:12.5px;line-height:1.5;color:var(--w-ink-2);margin:6px 0 0; + display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden} +.tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px} +.tag{display:inline-block;padding:1px 7px;border-radius:3px; + background:var(--w-surface-2);color:var(--w-ink-3);font-size:10.5px;font-weight:500} +.label{font-size:10px;text-transform:uppercase;letter-spacing:0.08em; + color:var(--w-ink-3);margin:8px 0 4px;font-weight:600} +.sess{font-size:12px;color:var(--w-ink-2);padding:2px 0; + display:flex;gap:8px;align-items:baseline} +.sess-time{font-family:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace; + color:var(--w-ink-3);font-variant-numeric:tabular-nums;flex-shrink:0;font-size:11.5px} +.link,.link:visited,.link:active{color:var(--w-accent);text-decoration:none; + font-size:12px;font-weight:500;display:inline-flex;align-items:center;gap:2px} .link:hover{text-decoration:underline} -.session-card{border:var(--border-width-regular,1px) solid var(--color-border-primary,#334155);border-radius:var(--border-radius-md,8px);padding:12px;margin-bottom:8px} -.session-title{font-size:14px;font-weight:var(--font-weight-semibold,600);margin-bottom:4px;color:var(--color-text-primary,#e2e8f0)} -.session-info{font-size:var(--font-text-xs-size,12px);color:var(--color-text-secondary,#94a3b8);display:flex;gap:8px;flex-wrap:wrap;margin-bottom:4px} -.badge{display:inline-block;padding:1px 6px;border-radius:var(--border-radius-xs,4px);font-size:9px;font-weight:var(--font-weight-semibold,600);text-transform:uppercase;white-space:nowrap;max-width:80px;overflow:hidden;text-overflow:ellipsis;vertical-align:middle} -.badge-keynote{background:#eab30822;color:#eab308} -.badge-talk{background:var(--color-background-tertiary,#1e293b);color:var(--color-text-accent,#818cf8)} -.badge-workshop{background:#22c55e22;color:#22c55e} -.badge-sponsor_activity{background:#f9731622;color:#f97316} -.time-slot{font-size:var(--font-text-xs-size,12px);font-weight:var(--font-weight-semibold,600);color:var(--color-text-accent,#818cf8);margin:10px 0 4px;padding-bottom:4px;border-bottom:var(--border-width-regular,1px) solid var(--color-border-primary,#334155)} -.schedule-item{font-size:var(--font-text-xs-size,12px);padding:3px 0;display:flex;gap:8px;color:var(--color-text-primary,#e2e8f0)} -.schedule-title{flex:1} -.schedule-room{color:var(--color-text-tertiary,#64748b);font-size:11px} -.meta{font-size:11px;color:var(--color-text-secondary,#94a3b8);margin-bottom:8px} +.row-link{margin-top:6px} + +/* ---------- session rows ---------- */ +.sess-row{padding:11px 0;border-bottom:1px solid var(--w-rule-soft);position:relative} +.sess-row:last-child{border-bottom:none;padding-bottom:0} +.sess-row:first-of-type{padding-top:0} +.sess-row[data-keynote="true"]::before{content:"";position:absolute;left:-12px;top:0;bottom:0; + width:3px;background:var(--w-accent)} +.sess-title{font-size:14px;font-weight:500;color:var(--w-ink);line-height:1.35; + display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} +.sess-title.link{font-weight:500;font-size:14px;display:block} +.sess-info{display:flex;flex-wrap:wrap;gap:4px 10px;margin-top:4px; + font-size:12px;color:var(--w-ink-2);align-items:baseline} +.sess-time-r{font-family:ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace; + color:var(--w-ink-3);font-size:11.5px;font-variant-numeric:tabular-nums} +.sess-room{color:var(--w-ink-2)} +.sess-speakers{font-size:12px;color:var(--w-ink-3);margin-top:2px;line-height:1.4} + +/* ---------- badges ---------- */ +.badge{display:inline-flex;align-items:center;font-size:10px;font-weight:600; + text-transform:uppercase;letter-spacing:0.06em;padding:2px 6px;border-radius:4px; + background:var(--w-surface-3);color:var(--w-ink-2);white-space:nowrap;vertical-align:middle} +.badge-keynote{background:var(--w-accent-soft);color:var(--w-accent)} +.badge-workshop{background:color-mix(in srgb,#15803d 14%,transparent);color:#15803d} +.badge-social{background:color-mix(in srgb,#ec4899 14%,transparent);color:#ec4899} +.badge-sponsor_activity{background:color-mix(in srgb,#b45309 14%,transparent);color:#b45309} + +/* ---------- schedule ---------- */ +.day-head{font-size:14px;font-weight:600;color:var(--w-ink);margin-bottom:12px; + letter-spacing:0.01em} +.time-slot{display:flex;align-items:baseline;gap:10px; + margin:14px 0 4px;padding:6px 0 6px;border-bottom:1px solid var(--w-rule)} +.time-slot:first-of-type{margin-top:0} +.time-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(--w-ink); + font-variant-numeric:tabular-nums} +.schedule-item{padding:7px 0;border-bottom:1px solid var(--w-rule-soft); + font-size:13px;color:var(--w-ink);position:relative} +.schedule-item:last-child{border-bottom:none} +.schedule-item[data-keynote="true"]::before{content:"";position:absolute;left:-12px;top:0;bottom:0; + width:3px;background:var(--w-accent)} +.schedule-title{display:block;font-size:13.5px;line-height:1.35;color:var(--w-ink);font-weight:500} +.schedule-meta{display:flex;flex-wrap:wrap;gap:4px 10px;margin-top:3px; + font-size:11.5px;color:var(--w-ink-2);align-items:baseline} +.schedule-room{color:var(--w-ink-3)} +.schedule-speakers{font-size:11.5px;color:var(--w-ink-3);margin-top:2px;line-height:1.4} +""" + +# Boot snippet for chat widgets: applies data-theme based on host theme so the +# widget palette adapts when the user toggles light/dark. +_WIDGET_THEME_BOOT = """\ +function applyTheme(t){ + var mode=(t&&t.mode==="dark")?"dark":"light"; + document.documentElement.setAttribute("data-theme",mode); +} """ def _wrap_widget(body_html: str, widget_name: str = "widget") -> str: synapse_js = _SYNAPSE_JS - widget_js = f"Synapse.connect({{name:'{widget_name}',version:'1.0.0',autoResize:true}});" + widget_js = ( + f"{_WIDGET_THEME_BOOT}" + f"Synapse.connect({{name:'{widget_name}',version:'1.0.0',autoResize:true," + f"on:{{'theme-changed':function(t){{applyTheme(t);}}}}" + f"}}).then(function(a){{ applyTheme(a.theme); window._app=a; }});" + ) return ( - "" + "" "" f"" f"{body_html}" @@ -475,37 +536,47 @@ def _wrap_widget(body_html: str, widget_name: str = "widget") -> str: def _render_session_list(sessions: list[dict]) -> str: import html as html_mod - h = f'
{len(sessions)} sessions
' + h = f'
{len(sessions)} {"session" if len(sessions) == 1 else "sessions"}
' for s in sessions[:10]: title = html_mod.escape(s.get("title", "")) stype = s.get("session_type", "") room = html_mod.escape(s.get("room", "")) - start = s.get("start_time", "") - end = s.get("end_time", "") - day = s.get("day", "")[-5:] + start = html_mod.escape(s.get("start_time", "")) + end = html_mod.escape(s.get("end_time", "")) + day = html_mod.escape(s.get("day", "")[-5:]) sched_url = s.get("sched_url", "") speakers = ", ".join(html_mod.escape(sp.get("name", "")) for sp in s.get("speakers", [])) if not speakers: speakers = ", ".join(html_mod.escape(n) for n in s.get("speaker_names", [])) - desc = html_mod.escape(s.get("description_preview", "")[:150]) + desc = html_mod.escape(s.get("description_preview", "")[:140]) + 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 = f'{title}' + title_html = f'
{title}
' + + meta_parts = [] + if stype: + meta_parts.append( + f'{html_mod.escape(stype.replace("_", " "))}' + ) + if room: + meta_parts.append(f'{room}') + if day or time_str: + sep = " · " if day and time_str else "" + meta_parts.append(f'{day}{sep}{time_str}') h += ( - 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 })} > -
-
{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}

- {/* 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 && ( +
+
Where
+
{sess.room}
+
+ )} +
- {/* Speakers */} - {(detailSpeakers.length > 0 || speakerNames.length > 0) && ( -
-
Speakers
- {detailSpeakers.length > 0 ? ( - detailSpeakers.map((sp) => ( -
- {sp.photo_url ? ( - {sp.name} - ) : ( -
{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} + ) : ( +
{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 */} -
+
{sess.sched_url && ( + 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) => ( - ))}
- {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]) => ( -
- -
-
- - {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}`}
)}
+
); + 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)} /> -
- - -
+ {speakerQuery && ( + + )}
- {speakersTool.isPending ? ( -
Loading speakers...
+
+ + +
+ + {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} + {sp.name} ) : ( -
- {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 && ( + + )}
+ + {!searchQuery.trim() && ( + <> +
+ {SEARCH_SUGGESTIONS.map((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 ( <> -
-
- - - - -
+
+
+ - {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" },