@medalsocial/meda 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/chat/latency-badge.js +1 -1
  2. package/dist/chat/latency-breakdown.js +1 -1
  3. package/dist/chat/turn-card.js +1 -1
  4. package/dist/shell/app-shell-auth.d.ts +5 -0
  5. package/dist/shell/app-shell-auth.js +50 -0
  6. package/dist/shell/app-shell-chat.d.ts +6 -0
  7. package/dist/shell/app-shell-chat.js +9 -0
  8. package/dist/shell/app-shell-workspace.d.ts +10 -0
  9. package/dist/shell/app-shell-workspace.js +55 -0
  10. package/dist/shell/app-shell.d.ts +18 -2
  11. package/dist/shell/app-shell.js +17 -2
  12. package/dist/shell/context-rail.js +1 -1
  13. package/dist/shell/icon-rail.js +1 -1
  14. package/dist/shell/index.d.ts +1 -6
  15. package/dist/shell/index.js +0 -5
  16. package/dist/shell/{mobile → internal}/mobile-bottom-nav.d.ts +9 -1
  17. package/dist/shell/{mobile → internal}/mobile-bottom-nav.js +5 -2
  18. package/dist/shell/{mobile → internal}/mobile-drawers.d.ts +7 -1
  19. package/dist/shell/{mobile → internal}/mobile-drawers.js +18 -4
  20. package/dist/shell/internal/mobile-header.js +22 -0
  21. package/dist/shell/types.d.ts +35 -0
  22. package/dist/timeline/event-card.js +1 -1
  23. package/dist/timeline/live-indicator.js +1 -1
  24. package/dist/timeline/scrub-bar.js +1 -1
  25. package/package.json +5 -1
  26. package/dist/shell/mobile/mobile-header.js +0 -22
  27. package/dist/shell/navigation-area.d.ts +0 -4
  28. package/dist/shell/navigation-area.js +0 -4
  29. package/dist/shell/public.d.ts +0 -16
  30. package/dist/shell/public.js +0 -20
  31. package/dist/shell/shell-app-rail.d.ts +0 -18
  32. package/dist/shell/shell-app-rail.js +0 -27
  33. package/dist/shell/shell-auth-frame.d.ts +0 -22
  34. package/dist/shell/shell-auth-frame.js +0 -60
  35. package/dist/shell/shell-desktop-panel-dock.d.ts +0 -14
  36. package/dist/shell/shell-desktop-panel-dock.js +0 -43
  37. package/dist/shell/shell-frame.d.ts +0 -13
  38. package/dist/shell/shell-frame.js +0 -4
  39. package/dist/shell/shell-module-nav.d.ts +0 -29
  40. package/dist/shell/shell-module-nav.js +0 -16
  41. package/dist/shell/shell-panel-rail.d.ts +0 -9
  42. package/dist/shell/shell-panel-rail.js +0 -17
  43. package/dist/shell/shell-scrollable-content.d.ts +0 -9
  44. package/dist/shell/shell-scrollable-content.js +0 -7
  45. package/dist/shell/shell-state.d.ts +0 -39
  46. package/dist/shell/shell-state.js +0 -135
  47. package/dist/shell/shell-tab-bar.d.ts +0 -18
  48. package/dist/shell/shell-tab-bar.js +0 -27
  49. package/dist/shell/workbench-layout.d.ts +0 -10
  50. package/dist/shell/workbench-layout.js +0 -29
  51. package/dist/styles.css +0 -26
  52. /package/dist/shell/{mobile → internal}/mobile-header.d.ts +0 -0
@@ -19,5 +19,5 @@ export function LatencyBadge({ kind, ms, className }) {
19
19
  'inline-flex items-center gap-1.5 text-[11px] tabular-nums',
20
20
  meta.tone,
21
21
  className ?? '',
22
- ].join(' '), children: [_jsx("span", { "aria-hidden": "true", className: "size-1 rounded-full bg-current" }), meta.label, " ", fmt(ms)] }));
22
+ ].join(' '), children: [_jsx("span", { "aria-hidden": "true", className: "size-1 rounded-full bg-current" }), meta.label, " ", _jsx("span", { "data-chromatic": "ignore", children: fmt(ms) })] }));
23
23
  }
@@ -5,5 +5,5 @@ export function LatencyBreakdown({ sttMs, claudeMs, ttsMs, showLegend = true, cl
5
5
  const sttPct = (sttMs / total) * 100;
6
6
  const claudePct = (claudeMs / total) * 100;
7
7
  const ttsPct = (ttsMs / total) * 100;
8
- return (_jsxs("div", { className: className, children: [_jsxs("div", { className: "flex h-1.5 gap-0.5 overflow-hidden rounded-sm bg-muted", children: [_jsx("span", { "data-segment": "stt", className: "block bg-sky-400", style: { width: `${sttPct}%` } }), _jsx("span", { "data-segment": "claude", className: "block bg-primary", style: { width: `${claudePct}%` } }), _jsx("span", { "data-segment": "tts", className: "block bg-emerald-500", style: { width: `${ttsPct}%` } })] }), showLegend && (_jsxs("div", { className: "mt-1.5 flex flex-wrap gap-3 text-[11px] tabular-nums text-muted-foreground", children: [_jsx(LatencyBadge, { kind: "stt", ms: sttMs }), _jsx(LatencyBadge, { kind: "claude", ms: claudeMs }), _jsx(LatencyBadge, { kind: "tts", ms: ttsMs })] }))] }));
8
+ return (_jsxs("div", { className: className, children: [_jsxs("div", { "data-chromatic": "ignore", className: "flex h-1.5 gap-0.5 overflow-hidden rounded-sm bg-muted", children: [_jsx("span", { "data-segment": "stt", className: "block bg-sky-400", style: { width: `${sttPct}%` } }), _jsx("span", { "data-segment": "claude", className: "block bg-primary", style: { width: `${claudePct}%` } }), _jsx("span", { "data-segment": "tts", className: "block bg-emerald-500", style: { width: `${ttsPct}%` } })] }), showLegend && (_jsxs("div", { className: "mt-1.5 flex flex-wrap gap-3 text-[11px] tabular-nums text-muted-foreground", children: [_jsx(LatencyBadge, { kind: "stt", ms: sttMs }), _jsx(LatencyBadge, { kind: "claude", ms: claudeMs }), _jsx(LatencyBadge, { kind: "tts", ms: ttsMs })] }))] }));
9
9
  }
@@ -21,5 +21,5 @@ export function TurnCard({ turn, startedAtRef, tz, onPlay, className }) {
21
21
  return (_jsxs("div", { className: [
22
22
  'grid grid-cols-[64px_1fr] gap-5 border-b border-border py-4 last:border-b-0',
23
23
  className ?? '',
24
- ].join(' '), children: [_jsxs("div", { className: "pt-1 text-[11px] tabular-nums text-muted-foreground", children: [_jsx("strong", { className: "mb-0.5 block text-[12px] font-medium text-muted-foreground", children: formatClock(new Date(turn.startedAt), { tz }) }), formatRelativeOffset(offset)] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-1.5 flex items-center gap-2", children: [_jsx("span", { className: ['text-[11px] font-semibold uppercase tracking-wider', speakerColor].join(' '), children: label }), turn.modelLabel && (_jsx("span", { className: "rounded bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground", children: turn.modelLabel })), turn.spokenSeconds != null && turn.speaker === 'user' && (_jsxs("span", { className: "rounded bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground", children: ["spoken \u00B7 ", turn.spokenSeconds.toFixed(1), "s"] }))] }), _jsxs("p", { className: "text-[15px] leading-relaxed text-foreground", "data-streaming": String(turn.streaming ?? false), children: [turn.text, turn.streaming && (_jsx("span", { "aria-hidden": "true", className: "ml-0.5 animate-pulse text-primary", children: "\u258D" }))] }), turn.toolCalls?.map((c) => (_jsx(ToolCallBlock, { call: c }, c.id))), (turn.audioUrl || turn.latency) && (_jsxs("div", { className: "mt-2.5 flex flex-wrap items-center gap-4 text-[11px] text-muted-foreground", children: [turn.audioUrl && (_jsx("button", { type: "button", "aria-label": "Play this turn", onClick: () => onPlay?.(turn.id), className: "inline-flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground hover:text-foreground", children: _jsx(Play, { className: "size-3" }) })), turn.latency?.sttMs != null && _jsx(LatencyBadge, { kind: "stt", ms: turn.latency.sttMs }), turn.latency?.claudeMs != null && (_jsx(LatencyBadge, { kind: "claude", ms: turn.latency.claudeMs })), turn.latency?.ttsMs != null && _jsx(LatencyBadge, { kind: "tts", ms: turn.latency.ttsMs })] }))] })] }));
24
+ ].join(' '), children: [_jsxs("div", { className: "pt-1 text-[11px] tabular-nums text-muted-foreground", children: [_jsx("strong", { className: "mb-0.5 block text-[12px] font-medium text-muted-foreground", children: formatClock(new Date(turn.startedAt), { tz }) }), formatRelativeOffset(offset)] }), _jsxs("div", { children: [_jsxs("div", { className: "mb-1.5 flex items-center gap-2", children: [_jsx("span", { className: ['text-[11px] font-semibold uppercase tracking-wider', speakerColor].join(' '), children: label }), turn.modelLabel && (_jsx("span", { className: "rounded bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground", children: turn.modelLabel })), turn.spokenSeconds != null && turn.speaker === 'user' && (_jsxs("span", { className: "rounded bg-muted px-1.5 py-px text-[10px] font-medium text-muted-foreground", children: ["spoken \u00B7 ", turn.spokenSeconds.toFixed(1), "s"] }))] }), _jsxs("p", { className: "text-[15px] leading-relaxed text-foreground", "data-streaming": String(turn.streaming ?? false), children: [turn.text, turn.streaming && (_jsx("span", { "aria-hidden": "true", "data-chromatic": "ignore", className: "ml-0.5 animate-pulse text-primary", children: "\u258D" }))] }), turn.toolCalls?.map((c) => (_jsx(ToolCallBlock, { call: c }, c.id))), (turn.audioUrl || turn.latency) && (_jsxs("div", { className: "mt-2.5 flex flex-wrap items-center gap-4 text-[11px] text-muted-foreground", children: [turn.audioUrl && (_jsx("button", { type: "button", "aria-label": "Play this turn", onClick: () => onPlay?.(turn.id), className: "inline-flex size-6 items-center justify-center rounded-full bg-muted text-muted-foreground hover:text-foreground", children: _jsx(Play, { className: "size-3" }) })), turn.latency?.sttMs != null && _jsx(LatencyBadge, { kind: "stt", ms: turn.latency.sttMs }), turn.latency?.claudeMs != null && (_jsx(LatencyBadge, { kind: "claude", ms: turn.latency.claudeMs })), turn.latency?.ttsMs != null && _jsx(LatencyBadge, { kind: "tts", ms: turn.latency.ttsMs })] }))] })] }));
25
25
  }
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AppShellAuthConfig } from './types.js';
3
+ export declare function AppShellAuth({ children, title, description, brandName, brandMark, eyebrow, preview, actions, }: AppShellAuthConfig & {
4
+ children: ReactNode;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { BarChart3, CheckCircle2, Mail, PanelRightOpen, Search, Send, Sparkles, UsersRound, } from 'lucide-react';
4
+ import { cn } from '../lib/utils.js';
5
+ function DefaultBrandMark() {
6
+ return (_jsx("span", { className: "inline-flex size-10 items-center justify-center rounded-lg bg-white/14 text-white ring-1 ring-white/20 shadow-lg shadow-black/20", children: _jsx(Sparkles, { className: "size-5", "aria-hidden": true }) }));
7
+ }
8
+ export function AppShellAuth({ children, title, description, brandName = 'Meda', brandMark, eyebrow, preview, actions, }) {
9
+ const resolvedBrandMark = brandMark ?? _jsx(DefaultBrandMark, {});
10
+ return (_jsxs("section", { "data-testid": "app-shell-auth", className: "grid min-h-screen overflow-hidden bg-background text-foreground lg:grid-cols-[minmax(0,1.08fr)_minmax(420px,0.92fr)]", children: [_jsxs("div", { className: "relative flex min-h-[24rem] flex-col overflow-hidden bg-[radial-gradient(circle_at_24%_18%,var(--color-info-500)_0,transparent_25%),radial-gradient(circle_at_84%_24%,var(--color-brand-400)_0,transparent_28%),linear-gradient(135deg,var(--color-brand-800),var(--color-brand-700)_42%,var(--color-brand-500))] px-6 py-6 text-white sm:min-h-[30rem] sm:px-8 lg:min-h-screen lg:px-10 lg:py-8", children: [_jsx("div", { className: "pointer-events-none absolute inset-0 opacity-[0.07]", style: {
11
+ backgroundImage: "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")",
12
+ } }), _jsxs("div", { className: "relative z-10 flex items-center gap-3", children: [resolvedBrandMark, _jsx("span", { className: "text-lg font-semibold tracking-normal", children: brandName })] }), _jsxs("div", { className: "relative z-10 flex flex-1 flex-col justify-center gap-8 py-10 lg:py-12", children: [_jsxs("div", { className: "max-w-2xl", children: [eyebrow && (_jsx("p", { className: "mb-4 text-sm font-semibold uppercase tracking-[0.12em] text-white/70", children: eyebrow })), _jsx("h1", { className: "text-4xl font-bold leading-tight tracking-normal text-white sm:text-5xl", children: title }), description && (_jsx("p", { className: "mt-4 max-w-xl text-base leading-7 text-white/72 sm:text-lg", children: description }))] }), _jsx("div", { className: "w-full max-w-3xl", children: preview ?? _jsx(DefaultAuthPreview, {}) })] })] }), _jsxs("div", { className: "relative flex min-h-screen flex-col bg-background px-6 py-6 sm:px-8 lg:px-12", children: [actions && _jsx("div", { className: "flex justify-end", children: actions }), _jsxs("div", { className: "mx-auto flex w-full max-w-md flex-1 flex-col justify-center py-10", children: [_jsx("div", { className: "mb-8 flex justify-center lg:hidden", children: resolvedBrandMark }), children] })] })] }));
13
+ }
14
+ function DefaultAuthPreview() {
15
+ return (_jsx("div", { "data-testid": "app-shell-auth-default-preview", className: "relative overflow-hidden rounded-[1.5rem] border border-white/16 bg-[color-mix(in_srgb,var(--background)_88%,transparent)] p-2 text-foreground shadow-2xl shadow-black/30 backdrop-blur", "aria-hidden": true, children: _jsxs("div", { className: "overflow-hidden rounded-[1.125rem] border border-border/70 bg-background shadow-sm", children: [_jsxs("div", { className: "flex h-9 items-center justify-between bg-card/90 px-3", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "size-2 rounded-full bg-rose-400" }), _jsx("span", { className: "size-2 rounded-full bg-amber-400" }), _jsx("span", { className: "size-2 rounded-full bg-emerald-400" })] }), _jsx("span", { className: "hidden rounded-full bg-muted px-2 py-0.5 text-[9px] font-medium text-muted-foreground sm:inline-flex", children: "app.medal.social" })] }), _jsxs("div", { "data-testid": "app-shell-auth-preview-shell-header", className: "flex h-12 items-center justify-between border-shell-border border-t border-b bg-shell-header px-3", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "inline-flex size-7 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground", children: _jsx(Sparkles, { className: "size-3.5", "aria-hidden": "true" }) }), _jsx("span", { className: "truncate text-[11px] font-semibold", children: "Medal Social" }), _jsx("div", { className: "ml-2 hidden items-center gap-0.5 rounded-full bg-muted/70 p-0.5 sm:flex", children: PREVIEW_TABS.map((tab) => (_jsx("span", { className: cn('rounded-full px-2 py-1 text-[10px] font-medium', tab === 'Inbox'
16
+ ? 'bg-background text-foreground shadow-sm'
17
+ : 'text-muted-foreground'), children: tab }, tab))) })] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "hidden rounded-lg bg-primary px-2.5 py-1.5 text-[9px] font-semibold text-primary-foreground sm:inline-flex", children: "+ New" }), _jsx("span", { "data-testid": "app-shell-auth-preview-panel-toggle", className: "inline-flex size-8 items-center justify-center rounded-lg border border-border bg-card text-muted-foreground shadow-sm", children: _jsx(PanelRightOpen, { className: "size-3.5", "aria-hidden": "true" }) })] })] }), _jsxs("div", { className: "grid min-h-[19.75rem] grid-cols-[44px_minmax(0,1fr)] gap-2 bg-shell-main p-2 sm:grid-cols-[48px_138px_minmax(0,1fr)] lg:grid-cols-[52px_156px_minmax(0,1fr)]", children: [_jsxs("aside", { "data-testid": "app-shell-auth-preview-icon-rail", className: "flex flex-col items-center gap-1 rounded-2xl bg-shell-rail p-1.5 shadow-sm ring-1 ring-border/70", children: [PREVIEW_RAIL_ITEMS.map((item) => (_jsx(PreviewRailItem, { icon: item.icon, label: item.label, active: item.active, badge: item.badge }, item.label))), _jsx("span", { className: "mt-auto inline-flex size-8 items-center justify-center rounded-lg bg-muted text-[10px] font-semibold text-muted-foreground", children: "AS" })] }), _jsxs("aside", { "data-testid": "app-shell-auth-preview-context-rail", className: "hidden overflow-hidden rounded-2xl bg-shell-context shadow-sm ring-1 ring-border/70 sm:flex sm:flex-col", children: [_jsxs("div", { className: "border-shell-border border-b px-3 py-3", children: [_jsx("p", { className: "text-[11px] font-semibold text-foreground", children: "Customer OS" }), _jsx("p", { className: "mt-0.5 truncate text-[9px] text-muted-foreground", children: "Auto-CS workspace" })] }), _jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: PREVIEW_CONTEXT_ITEMS.map((item) => (_jsx(PreviewContextItem, { ...item }, item.label))) }), _jsxs("div", { className: "mt-auto border-shell-border border-t p-2", children: [_jsx("p", { className: "text-[9px] font-medium text-muted-foreground", children: "Next sync" }), _jsx("p", { className: "mt-1 truncate text-[10px] font-semibold text-foreground", children: "12 campaigns" })] })] }), _jsxs("main", { className: "min-w-0 overflow-hidden rounded-2xl bg-background/80 p-3 shadow-sm ring-1 ring-border/70", children: [_jsxs("div", { className: "mb-3 flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "text-[9px] font-medium uppercase text-muted-foreground", children: "Inbox command center" }), _jsx("p", { className: "mt-1 truncate text-sm font-semibold", children: "Launch queue and support" })] }), _jsx("span", { className: "rounded-full bg-success/12 px-2 py-1 text-[9px] font-semibold text-success", children: "Live" })] }), _jsxs("div", { className: "mb-3 flex h-9 items-center gap-2 rounded-xl border border-border bg-card px-2.5 text-muted-foreground shadow-sm", children: [_jsx(Search, { className: "size-3.5 shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "truncate text-[10px]", children: "Ask Meda to draft a customer reply..." }), _jsx("kbd", { className: "ml-auto hidden rounded-md bg-muted px-1.5 py-0.5 font-mono text-[8px] text-muted-foreground sm:inline", children: "CMD K" })] }), _jsx("div", { className: "grid gap-2 sm:grid-cols-3", children: PREVIEW_METRICS.map((metric) => (_jsxs("div", { className: "rounded-xl border border-border bg-card p-2.5", children: [_jsx("p", { className: "text-[9px] font-medium text-muted-foreground", children: metric.label }), _jsx("p", { className: "mt-1 text-base font-semibold", children: metric.value })] }, metric.label))) }), _jsxs("div", { className: "mt-3 grid gap-2 lg:grid-cols-[minmax(0,1fr)_112px]", children: [_jsxs("div", { className: "rounded-xl border border-border bg-card p-2.5", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("p", { className: "text-[11px] font-semibold", children: "Priority work" }), _jsx("p", { className: "text-[9px] text-muted-foreground", children: "Updated now" })] }), _jsx("div", { className: "space-y-2", children: PREVIEW_ACTIVITY.map((item) => (_jsx(PreviewActivityRow, { label: item }, item))) })] }), _jsxs("div", { className: "hidden rounded-xl border border-border bg-card p-2.5 lg:block", children: [_jsx("p", { className: "text-[10px] font-semibold", children: "Publish health" }), _jsx("div", { className: "mt-3 h-1.5 overflow-hidden rounded-full bg-muted", children: _jsx("div", { className: "h-full w-[72%] rounded-full bg-primary" }) }), _jsx("p", { className: "mt-2 text-[9px] leading-4 text-muted-foreground", children: "Response SLA and scheduled posts are on track." })] })] })] })] })] }) }));
18
+ }
19
+ function PreviewRailItem({ icon: Icon, label, active, badge, }) {
20
+ return (_jsxs("span", { className: cn('relative inline-flex size-8 items-center justify-center rounded-lg transition-colors', active ? 'bg-primary/12 text-primary' : 'text-muted-foreground'), title: label, children: [_jsx(Icon, { className: "size-4", "aria-hidden": "true" }), badge && _jsx("span", { className: "absolute top-0.5 right-0.5 size-1.5 rounded-full bg-primary" })] }));
21
+ }
22
+ function PreviewContextItem({ icon: Icon, label, active, shortcut, }) {
23
+ return (_jsxs("span", { className: cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-[10px] transition-colors', active ? 'bg-primary/10 font-semibold text-primary' : 'text-muted-foreground'), children: [_jsx(Icon, { className: "size-3.5 shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "truncate", children: label }), shortcut && (_jsx("kbd", { className: "ml-auto font-mono text-[8px] text-muted-foreground", children: shortcut }))] }));
24
+ }
25
+ function PreviewActivityRow({ label }) {
26
+ return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary", children: _jsx(CheckCircle2, { className: "size-3", "aria-hidden": "true" }) }), _jsx("p", { className: "truncate text-[10px] text-muted-foreground", children: label })] }));
27
+ }
28
+ const PREVIEW_TABS = ['Home', 'Inbox', 'Campaigns'];
29
+ const PREVIEW_RAIL_ITEMS = [
30
+ { icon: BarChart3, label: 'Overview' },
31
+ { icon: Mail, label: 'Inbox', active: true, badge: '4' },
32
+ { icon: Send, label: 'Posts' },
33
+ { icon: UsersRound, label: 'Contacts' },
34
+ ];
35
+ const PREVIEW_CONTEXT_ITEMS = [
36
+ { icon: BarChart3, label: 'Overview', shortcut: '1' },
37
+ { icon: Mail, label: 'Inbox', active: true, shortcut: '2' },
38
+ { icon: Send, label: 'Posts', shortcut: '3' },
39
+ { icon: UsersRound, label: 'Contacts', shortcut: '4' },
40
+ ];
41
+ const PREVIEW_METRICS = [
42
+ { label: 'Queued posts', value: '18' },
43
+ { label: 'Open tickets', value: '42' },
44
+ { label: 'Reach', value: '9.8k' },
45
+ ];
46
+ const PREVIEW_ACTIVITY = [
47
+ 'Campaign approved for publishing',
48
+ 'Lead segment synced with CRM',
49
+ 'Automated reply drafted for review',
50
+ ];
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface AppShellChatProps {
3
+ globalActions?: ReactNode;
4
+ children: ReactNode;
5
+ }
6
+ export declare function AppShellChat({ globalActions, children }: AppShellChatProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from '../lib/utils.js';
4
+ import { useShellViewport } from './use-shell-viewport.js';
5
+ export function AppShellChat({ globalActions, children }) {
6
+ const viewport = useShellViewport();
7
+ const isMobile = viewport === 'mobile';
8
+ return (_jsxs("section", { "data-testid": "app-shell-chat", className: "flex h-screen flex-col bg-background text-foreground", children: [_jsxs("header", { className: "flex h-12 items-center justify-between border-b border-border px-4", children: [_jsx("span", { className: "text-sm font-medium text-foreground", children: "Chat" }), globalActions ? _jsx("div", { className: "flex items-center gap-2", children: globalActions }) : null] }), _jsx("div", { className: cn('flex-1 overflow-y-auto', isMobile ? 'px-2 py-3' : 'px-6 py-4'), children: children })] }));
9
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
3
+ export interface AppShellWorkspaceProps {
4
+ iconRail?: AppShellIconRailConfig;
5
+ contextRail?: AppShellContextRailConfig;
6
+ rightPanel?: AppShellRightPanelConfig;
7
+ globalActions?: ReactNode;
8
+ children: ReactNode;
9
+ }
10
+ export declare function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalActions, children, }: AppShellWorkspaceProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { LayoutGrid, Menu, PanelTop, Sparkles } from 'lucide-react';
4
+ import { ContextRail } from './context-rail.js';
5
+ import { IconRail } from './icon-rail.js';
6
+ import { MobileBottomNav } from './internal/mobile-bottom-nav.js';
7
+ import { MobileDrawers } from './internal/mobile-drawers.js';
8
+ import { MobileHeader } from './internal/mobile-header.js';
9
+ import { RightPanel } from './right-panel.js';
10
+ import { ShellHeader } from './shell-header.js';
11
+ import { ShellMain } from './shell-main.js';
12
+ import { useShellViewport } from './use-shell-viewport.js';
13
+ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalActions, children, }) {
14
+ const viewport = useShellViewport();
15
+ const isMobile = viewport === 'mobile';
16
+ // Derive the bottom-nav items from the variant config so each button maps
17
+ // to a drawer that actually has content. Without this filter, partial
18
+ // configs (e.g. iconRail only) would show Module/Panels/AI buttons that
19
+ // dispatch into the void.
20
+ const navItems = buildMobileNavItems(iconRail, contextRail, rightPanel);
21
+ const hasDrawerContent = navItems.length > 0;
22
+ // Mobile menu drawer needs both main and utility items — desktop IconRail
23
+ // shows both, so dropping utilityItems here would orphan items like Help/
24
+ // Settings on mobile. Concat preserves discoverability; visual separation
25
+ // (divider in the drawer between main and utility) is a future polish.
26
+ const mobileMenuItems = iconRail ? [...iconRail.mainItems, ...(iconRail.utilityItems ?? [])] : [];
27
+ // ONE tree shape across both viewports. ShellMain stays at a fixed position
28
+ // in its parent's children array so React preserves its subtree (and the
29
+ // user's `children` state) when useShellViewport flips after mount or on
30
+ // rotation. Conditional siblings render as `false` rather than disappearing.
31
+ //
32
+ // h-screen (not h-full) so this stays a bounded scroll container even when
33
+ // rendered without an explicit-height ancestor (tests, direct imports). The
34
+ // <AppShell> wrapper already enforces h-screen for the workspace variant, so
35
+ // nested viewport-height divs collapse cleanly — no double-scroll.
36
+ return (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions })), _jsxs("div", { className: "relative flex flex-1 overflow-hidden", children: [!isMobile && iconRail && (_jsx(IconRail, { mainItems: iconRail.mainItems, utilityItems: iconRail.utilityItems, footer: iconRail.footer, activeId: iconRail.activeId })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId })), _jsx(ShellMain, { layout: "workspace", children: children }), !isMobile && rightPanel && (_jsx(RightPanel, { panelViews: rightPanel.panelViews, defaultView: rightPanel.defaultView }))] }), isMobile && hasDrawerContent && _jsx(MobileBottomNav, { items: navItems }), isMobile && hasDrawerContent && (_jsx(MobileDrawers, { menuItems: mobileMenuItems, module: contextRail?.module, panelViews: rightPanel?.panelViews ?? [], defaultView: rightPanel?.defaultView }))] }));
37
+ }
38
+ function buildMobileNavItems(iconRail, contextRail, rightPanel) {
39
+ const items = [];
40
+ if (iconRail) {
41
+ items.push({ id: 'menu', label: 'Menu', icon: Menu, opens: 'menu-drawer' });
42
+ }
43
+ if (contextRail?.module) {
44
+ items.push({ id: 'module', label: 'Module', icon: LayoutGrid, opens: 'module-drawer' });
45
+ }
46
+ // Panels button only when there's an actual view to render — empty
47
+ // panelViews would open an empty drawer (dead-end tap).
48
+ if (rightPanel && rightPanel.panelViews.length > 0) {
49
+ items.push({ id: 'panels', label: 'Panels', icon: PanelTop, opens: 'panels-drawer' });
50
+ if (rightPanel.panelViews.some((v) => v.id === 'ai')) {
51
+ items.push({ id: 'ai', label: 'AI', icon: Sparkles, opens: 'ai-drawer' });
52
+ }
53
+ }
54
+ return items;
55
+ }
@@ -1,9 +1,25 @@
1
1
  import type { ReactNode } from 'react';
2
- export declare function AppShell({ children, className }: {
2
+ import type { AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
3
+ interface AppShellBaseProps {
3
4
  children: ReactNode;
4
5
  className?: string;
5
- }): import("react/jsx-runtime").JSX.Element;
6
+ }
7
+ export type AppShellProps = AppShellBaseProps & ({
8
+ variant: 'auth';
9
+ auth: AppShellAuthConfig;
10
+ } | {
11
+ variant: 'workspace';
12
+ iconRail?: AppShellIconRailConfig;
13
+ contextRail?: AppShellContextRailConfig;
14
+ rightPanel?: AppShellRightPanelConfig;
15
+ globalActions?: ReactNode;
16
+ } | {
17
+ variant: 'chat';
18
+ globalActions?: ReactNode;
19
+ });
20
+ export declare function AppShell(props: AppShellProps): import("react/jsx-runtime").JSX.Element;
6
21
  export declare function AppShellBody({ children, className }: {
7
22
  children: ReactNode;
8
23
  className?: string;
9
24
  }): import("react/jsx-runtime").JSX.Element;
25
+ export {};
@@ -1,10 +1,25 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { cn } from '../lib/utils.js';
4
+ import { AppShellAuth } from './app-shell-auth.js';
5
+ import { AppShellChat } from './app-shell-chat.js';
6
+ import { AppShellWorkspace } from './app-shell-workspace.js';
4
7
  import { useMedaShell } from './shell-provider.js';
5
- export function AppShell({ children, className }) {
8
+ export function AppShell(props) {
6
9
  const { workspace, activeAppId } = useMedaShell();
7
- return (_jsx("div", { "data-meda-app": activeAppId, "data-meda-workspace": workspace.id, className: cn('h-screen overflow-hidden bg-background text-foreground', className), children: children }));
10
+ // Auth lets the form scroll past viewport (signup, dense forms, high zoom);
11
+ // workspace and chat fix the chrome to viewport height and let inner regions
12
+ // scroll independently.
13
+ const heightClass = props.variant === 'auth' ? 'min-h-screen' : 'h-screen overflow-hidden';
14
+ const wrapper = (content) => (_jsx("div", { "data-meda-app": activeAppId, "data-meda-workspace": workspace.id, "data-meda-variant": props.variant, className: cn(heightClass, 'bg-background text-foreground', props.className), children: content }));
15
+ switch (props.variant) {
16
+ case 'auth':
17
+ return wrapper(_jsx(AppShellAuth, { ...props.auth, children: props.children }));
18
+ case 'workspace':
19
+ return wrapper(_jsx(AppShellWorkspace, { iconRail: props.iconRail, contextRail: props.contextRail, rightPanel: props.rightPanel, globalActions: props.globalActions, children: props.children }));
20
+ case 'chat':
21
+ return wrapper(_jsx(AppShellChat, { globalActions: props.globalActions, children: props.children }));
22
+ }
8
23
  }
9
24
  export function AppShellBody({ children, className }) {
10
25
  return (_jsx("div", { className: cn('relative flex h-[calc(100vh-var(--shell-header-height))] overflow-hidden', className), children: children }));
@@ -66,7 +66,7 @@ export function ContextRail({ appId: _appId, module, hidden = false, collapsible
66
66
  setDisplayWidth(null);
67
67
  ctx.contextRail.setWidth(w);
68
68
  };
69
- return (_jsxs("aside", { "aria-label": module.label, className: cn('relative h-full shrink-0 overflow-hidden border-r border-shell-border bg-shell-context', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [_jsxs("div", { className: "border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] }), _jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: module.items.map((item) => {
69
+ return (_jsxs("aside", { "data-testid": "context-rail", "aria-label": module.label, className: cn('relative h-full shrink-0 overflow-hidden border-r border-shell-border bg-shell-context', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [_jsxs("div", { className: "border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] }), _jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: module.items.map((item) => {
70
70
  const isActive = item.id === activeItemId;
71
71
  const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
72
72
  ? 'bg-primary/10 text-primary'
@@ -34,5 +34,5 @@ export function IconRail({ mainItems, utilityItems = [], footer, activeId, rende
34
34
  renderLink({ item, isActive, className: klass, children: inner })) : (_jsx("a", { href: item.to, "aria-label": item.label, "aria-current": isActive ? 'page' : undefined, className: "contents", children: inner }));
35
35
  return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("span", { "data-testid": `icon-rail-trigger-${item.id}`, className: klass, children: linkContent }) }), _jsx(TooltipContent, { side: "right", children: item.label })] }, item.id));
36
36
  };
37
- return (_jsx(TooltipProvider, { children: _jsxs("nav", { "aria-label": "Primary", className: cn('flex h-full w-[var(--shell-rail-width)] shrink-0 flex-col items-center bg-shell-rail py-3.5', className), children: [_jsx("div", { className: "flex flex-col items-center gap-1", children: mainItems.map(renderItem) }), utilityItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(RailDivider, { pinnedBottom: pinnedBottom, onToggle: () => setPinnedBottom((prev) => !prev) }), _jsx("div", { "data-testid": "utility-items-wrapper", className: cn('flex flex-col items-center gap-1', pinnedBottom && 'mt-auto'), children: utilityItems.map(renderItem) })] })), footer && (_jsx("div", { className: cn('pt-3', pinnedBottom || utilityItems.length === 0 ? 'mt-auto' : ''), children: footer }))] }) }));
37
+ return (_jsx(TooltipProvider, { children: _jsxs("nav", { "data-testid": "icon-rail", "aria-label": "Primary", className: cn('flex h-full w-[var(--shell-rail-width)] shrink-0 flex-col items-center bg-shell-rail py-3.5', className), children: [_jsx("div", { className: "flex flex-col items-center gap-1", children: mainItems.map(renderItem) }), utilityItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(RailDivider, { pinnedBottom: pinnedBottom, onToggle: () => setPinnedBottom((prev) => !prev) }), _jsx("div", { "data-testid": "utility-items-wrapper", className: cn('flex flex-col items-center gap-1', pinnedBottom && 'mt-auto'), children: utilityItems.map(renderItem) })] })), footer && (_jsx("div", { className: cn('pt-3', pinnedBottom || utilityItems.length === 0 ? 'mt-auto' : ''), children: footer }))] }) }));
38
38
  }
@@ -6,19 +6,14 @@ export * as Extras from './extras/index.js';
6
6
  export { IconRail, RailDivider } from './icon-rail.js';
7
7
  export type { ShellStorageAdapter } from './layout-state.js';
8
8
  export { createLocalStorageAdapter } from './layout-state.js';
9
- export { MobileBottomNav } from './mobile/mobile-bottom-nav.js';
10
- export { MobileDrawers } from './mobile/mobile-drawers.js';
11
- export { MobileHeader } from './mobile/mobile-header.js';
12
9
  export { motion } from './motion.js';
13
10
  export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
14
11
  export { RightPanel } from './right-panel.js';
15
- export type { ShellAuthFrameProps, ShellAuthTheme, ShellAuthThemeToggleProps, } from './shell-auth-frame.js';
16
- export { ShellAuthFrame, ShellAuthThemeToggle } from './shell-auth-frame.js';
17
12
  export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-header.js';
18
13
  export { ShellMain } from './shell-main.js';
19
14
  export type { MedaShellProviderProps } from './shell-provider.js';
20
15
  export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
21
16
  export { DefaultThemeProvider, ThemeToggle, useTheme } from './theme.js';
22
17
  export { NextThemesAdapter } from './theme-next-themes.js';
23
- export type { AppDefinition, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
18
+ export type { AppDefinition, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellVariant, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
24
19
  export { useShellViewport } from './use-shell-viewport.js';
@@ -14,16 +14,11 @@ export * as Extras from './extras/index.js';
14
14
  export { IconRail, RailDivider } from './icon-rail.js';
15
15
  // Storage adapter (consumers may want to provide their own)
16
16
  export { createLocalStorageAdapter } from './layout-state.js';
17
- export { MobileBottomNav } from './mobile/mobile-bottom-nav.js';
18
- export { MobileDrawers } from './mobile/mobile-drawers.js';
19
- // Mobile (individual exports + namespaced fallback)
20
- export { MobileHeader } from './mobile/mobile-header.js';
21
17
  // Hooks + tokens
22
18
  export { motion } from './motion.js';
23
19
  // Resize primitives
24
20
  export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
25
21
  export { RightPanel } from './right-panel.js';
26
- export { ShellAuthFrame, ShellAuthThemeToggle } from './shell-auth-frame.js';
27
22
  // Header (and its individual children for advanced composition)
28
23
  export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-header.js';
29
24
  export { ShellMain } from './shell-main.js';
@@ -1,4 +1,12 @@
1
+ import type { MobileBottomNavItem } from '../types.js';
1
2
  export interface MobileBottomNavProps {
3
+ /**
4
+ * Override the items to render. When omitted, falls back to
5
+ * `ctx.mobileBottomNav` from the provider. AppShellWorkspace passes a
6
+ * derived list filtered to drawers that actually have content, so taps
7
+ * never dispatch into the void.
8
+ */
9
+ items?: MobileBottomNavItem[];
2
10
  className?: string;
3
11
  }
4
12
  /**
@@ -12,4 +20,4 @@ export interface MobileBottomNavProps {
12
20
  *
13
21
  * Hidden on non-mobile viewports and when the right panel is in fullscreen mode.
14
22
  */
15
- export declare function MobileBottomNav({ className }: MobileBottomNavProps): import("react/jsx-runtime").JSX.Element | null;
23
+ export declare function MobileBottomNav({ items, className }: MobileBottomNavProps): import("react/jsx-runtime").JSX.Element | null;
@@ -15,14 +15,17 @@ import { useShellViewport } from '../use-shell-viewport.js';
15
15
  *
16
16
  * Hidden on non-mobile viewports and when the right panel is in fullscreen mode.
17
17
  */
18
- export function MobileBottomNav({ className }) {
18
+ export function MobileBottomNav({ items, className }) {
19
19
  const ctx = useMedaShell();
20
20
  const band = useShellViewport();
21
21
  if (band !== 'mobile')
22
22
  return null;
23
23
  if (ctx.panel.mode === 'fullscreen')
24
24
  return null;
25
- return (_jsx("nav", { "aria-label": "Mobile navigation", className: cn('flex h-[var(--shell-bottom-nav-height)] items-center justify-around border-t border-border bg-card', className), children: ctx.mobileBottomNav.map((item) => (_jsx(MobileBottomNavButton, { item: item }, item.id))) }));
25
+ const resolvedItems = items ?? ctx.mobileBottomNav;
26
+ if (resolvedItems.length === 0)
27
+ return null;
28
+ return (_jsx("nav", { "data-testid": "mobile-bottom-nav", "aria-label": "Mobile navigation", className: cn('flex h-[var(--shell-bottom-nav-height)] items-center justify-around border-t border-border bg-card', className), children: resolvedItems.map((item) => (_jsx(MobileBottomNavButton, { item: item }, item.id))) }));
26
29
  }
27
30
  /** Long-press threshold in milliseconds */
28
31
  const LONG_PRESS_DURATION = 500;
@@ -8,6 +8,12 @@ export interface MobileDrawersProps {
8
8
  module?: ContextModule;
9
9
  /** Panels drawer source. */
10
10
  panelViews?: PanelView[];
11
+ /**
12
+ * Default panel view id. Used when ctx.panel.activeView is null so the
13
+ * mobile panels drawer opens to the same view as the desktop right panel.
14
+ * Without this, mobile would always fall back to panelViews[0].
15
+ */
16
+ defaultView?: string;
11
17
  /** Custom content drawers keyed by id from MobileBottomNavItem.opens render fn. */
12
18
  customContent?: Record<string, (close: () => void) => ReactNode>;
13
19
  className?: string;
@@ -17,4 +23,4 @@ export interface MobileDrawersProps {
17
23
  * any custom-content drawers. Mount once near the AppShell root; drawers
18
24
  * open/close via `ctx.mobileDrawer.open` provider state.
19
25
  */
20
- export declare function MobileDrawers({ menuItems, module, panelViews, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function MobileDrawers({ menuItems, module, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
@@ -9,7 +9,7 @@ import { useMedaShell } from '../shell-provider.js';
9
9
  * any custom-content drawers. Mount once near the AppShell root; drawers
10
10
  * open/close via `ctx.mobileDrawer.open` provider state.
11
11
  */
12
- export function MobileDrawers({ menuItems = [], module, panelViews = [], customContent = {}, }) {
12
+ export function MobileDrawers({ menuItems = [], module, panelViews = [], defaultView, customContent = {}, }) {
13
13
  const ctx = useMedaShell();
14
14
  const open = ctx.mobileDrawer.open;
15
15
  const setOpen = ctx.mobileDrawer.setOpen;
@@ -18,7 +18,7 @@ export function MobileDrawers({ menuItems = [], module, panelViews = [], customC
18
18
  workspaceId: ctx.workspace.id,
19
19
  appId: ctx.activeAppId,
20
20
  };
21
- return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module }), _jsx(PanelsDrawer, { open: open === 'panels-drawer', onClose: close, panelViews: panelViews, renderCtx: renderCtx }), _jsx(AiDrawer, { open: open === 'ai-drawer', onClose: close, panelViews: panelViews, renderCtx: renderCtx }), Object.entries(customContent).map(([id, renderFn]) => (_jsx(Drawer, { open: open === id, onOpenChange: (o) => !o && close(), direction: "bottom", children: _jsx(DrawerContent, { children: renderFn(close) }) }, id)))] }));
21
+ return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module }), _jsx(PanelsDrawer, { open: open === 'panels-drawer', onClose: close, panelViews: panelViews, defaultView: defaultView, renderCtx: renderCtx }), _jsx(AiDrawer, { open: open === 'ai-drawer', onClose: close, panelViews: panelViews, renderCtx: renderCtx }), Object.entries(customContent).map(([id, renderFn]) => (_jsx(Drawer, { open: open === id, onOpenChange: (o) => !o && close(), direction: "bottom", children: _jsx(DrawerContent, { children: renderFn(close) }) }, id)))] }));
22
22
  }
23
23
  // ---------------------------------------------------------------------------
24
24
  // Internal sub-drawers
@@ -37,10 +37,24 @@ function ModuleDrawer({ open, onClose, module, }) {
37
37
  return (_jsxs("a", { href: item.to, onClick: onClose, className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground", children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }, item.id));
38
38
  }) })] }) }));
39
39
  }
40
- function PanelsDrawer({ open, onClose, panelViews, renderCtx, }) {
40
+ function PanelsDrawer({ open, onClose, panelViews, defaultView, renderCtx, }) {
41
41
  const ctx = useMedaShell();
42
42
  const activeView = ctx.panel.activeView;
43
- const active = panelViews.find((v) => v.id === activeView) ?? panelViews[0];
43
+ const setActiveView = ctx.panel.setActiveView;
44
+ // Hydrate ctx.panel.activeView from defaultView when the drawer opens — matches
45
+ // desktop RightPanel behavior so the tab highlight (aria-current) and provider
46
+ // state stay in sync across viewports. Without this, mobile shows the right
47
+ // content but no tab is marked active.
48
+ useEffect(() => {
49
+ if (open && activeView == null && defaultView && panelViews.some((v) => v.id === defaultView)) {
50
+ setActiveView(defaultView);
51
+ }
52
+ }, [open, activeView, defaultView, panelViews, setActiveView]);
53
+ // Resolution order: explicit user selection → consumer-provided defaultView
54
+ // → first view. Keeps mobile parity with desktop where defaultView is honored.
55
+ const active = panelViews.find((v) => v.id === activeView) ??
56
+ (defaultView ? panelViews.find((v) => v.id === defaultView) : undefined) ??
57
+ panelViews[0];
44
58
  return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "bottom", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: active?.label ?? 'Panels' }), _jsx(DrawerDescription, { className: "sr-only", children: "Contextual panels for the current module." })] }), panelViews.length > 1 && (_jsx("div", { className: "flex items-center gap-1 border-b border-border px-3 py-2", children: panelViews.map((view) => (_jsx("button", { type: "button", onClick: () => ctx.panel.setActiveView(view.id), "aria-current": view.id === activeView ? 'true' : undefined, className: cn('rounded-md px-2 py-1 text-xs', view.id === activeView
45
59
  ? 'bg-accent text-accent-foreground'
46
60
  : 'text-muted-foreground hover:bg-accent'), children: view.label }, view.id))) })), _jsx("div", { className: "flex-1 overflow-y-auto p-3", children: active?.render(renderCtx) })] }) }));
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronLeft } from 'lucide-react';
4
+ import { cn } from '../../lib/utils.js';
5
+ import { useMedaShell } from '../shell-provider.js';
6
+ import { useShellViewport } from '../use-shell-viewport.js';
7
+ /**
8
+ * Mobile-only header.
9
+ *
10
+ * Root mode: workspace name + optional globalActions.
11
+ * Nested mode (when parentLabel is set): ← parentLabel · pageTitle back button.
12
+ *
13
+ * Renders nothing on non-mobile viewports.
14
+ */
15
+ export function MobileHeader({ parentLabel, parentTo: _parentTo, title, onBack, globalActions, className, }) {
16
+ const ctx = useMedaShell();
17
+ const band = useShellViewport();
18
+ const isNested = !!parentLabel;
19
+ if (band !== 'mobile')
20
+ return null;
21
+ return (_jsx("header", { "data-testid": "mobile-header", "data-meda-mobile-header": isNested ? 'nested' : 'root', className: cn('flex h-[var(--shell-header-height)] items-center justify-between border-b border-border bg-card px-3', className), children: isNested ? (_jsxs("button", { type: "button", onClick: onBack, "aria-label": "Go back", className: "flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm hover:bg-accent", children: [_jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "text-muted-foreground", children: parentLabel }), title && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground/40", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "text-foreground", children: title })] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [_jsx("span", { "aria-hidden": "true", children: ctx.workspace.icon }), _jsx("span", { children: ctx.workspace.name })] }), globalActions && _jsx("div", { className: "flex items-center gap-1", children: globalActions })] })) }));
22
+ }
@@ -62,3 +62,38 @@ export interface ThemeAdapter {
62
62
  setTheme: (t: 'light' | 'dark' | 'system') => void;
63
63
  resolvedTheme: 'light' | 'dark';
64
64
  }
65
+ /**
66
+ * AppShell renders one of three shell modes:
67
+ * - `auth` — split-screen marketing + form (signed-out)
68
+ * - `workspace` — header + rails + main + panel (signed-in work surface;
69
+ * collapses to mobile header + bottom nav + drawers on mobile)
70
+ * - `chat` — transcript-dominant layout (conversational AI)
71
+ */
72
+ export type AppShellVariant = 'auth' | 'workspace' | 'chat';
73
+ /** IconRail configuration for `<AppShell variant="workspace">`. */
74
+ export interface AppShellIconRailConfig {
75
+ mainItems: import('./icon-rail.js').IconRailItem[];
76
+ utilityItems?: import('./icon-rail.js').IconRailItem[];
77
+ footer?: ReactNode;
78
+ activeId?: string;
79
+ }
80
+ /** ContextRail configuration for `<AppShell variant="workspace">`. */
81
+ export interface AppShellContextRailConfig {
82
+ appId: string;
83
+ module: ContextModule;
84
+ activeItemId?: string;
85
+ }
86
+ /** RightPanel configuration for `<AppShell variant="workspace">`. */
87
+ export interface AppShellRightPanelConfig {
88
+ panelViews: PanelView[];
89
+ defaultView?: string;
90
+ }
91
+ export interface AppShellAuthConfig {
92
+ title: ReactNode;
93
+ description?: ReactNode;
94
+ brandName?: ReactNode;
95
+ brandMark?: ReactNode;
96
+ eyebrow?: ReactNode;
97
+ preview?: ReactNode;
98
+ actions?: ReactNode;
99
+ }
@@ -14,5 +14,5 @@ export function EventCard({ event, size = 'default', onSelect, className }) {
14
14
  return (_jsxs("button", { type: "button", "data-live": String(isLive), "data-selected": String(isSelected), "data-kind": event.kind, onClick: () => onSelect?.(event), className: cn('relative w-full cursor-pointer rounded-md border border-border bg-card text-left transition-[border-color,transform] duration-100', 'hover:translate-x-px hover:border-primary/40', sizeClasses, liveClasses, errorClasses, selectedClasses, className), children: [_jsx("span", { "aria-hidden": "true", className: "absolute -left-2.5 top-1/2 h-px w-2.5 bg-border" }), _jsxs("span", { className: "flex items-center gap-1.5 font-medium", children: [isLive && _jsx(LiveDot, {}), _jsx("span", { className: "min-w-0 flex-1 truncate", children: event.primary })] }), size === 'default' && event.secondary != null && (_jsx("span", { className: "mt-0.5 block text-[10px] text-muted-foreground tabular-nums", children: event.secondary }))] }));
15
15
  }
16
16
  function LiveDot() {
17
- return (_jsx("span", { "aria-hidden": "true", className: "inline-block size-1.5 shrink-0 animate-pulse rounded-full bg-success shadow-[0_0_0_3px_rgba(16,185,129,0.20)]" }));
17
+ return (_jsx("span", { "aria-hidden": "true", "data-chromatic": "ignore", className: "inline-block size-1.5 shrink-0 animate-pulse rounded-full bg-success shadow-[0_0_0_3px_rgba(16,185,129,0.20)]" }));
18
18
  }
@@ -3,5 +3,5 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { formatClock } from '../lib/format-time.js';
4
4
  import { cn } from '../lib/utils.js';
5
5
  export function LiveIndicator({ now, tz, className }) {
6
- return (_jsxs("div", { role: "status", "aria-live": "polite", className: cn('relative h-0 pointer-events-none', className), children: [_jsx("span", { className: "pointer-events-auto absolute left-0 top-0 inline-flex items-center rounded-md bg-success-700 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-white shadow-[0_6px_18px_rgba(16,185,129,0.35)]", children: "LIVE" }), _jsx("span", { "aria-hidden": "true", className: "absolute right-0 top-1/2 left-[60px] h-px bg-gradient-to-r from-success via-success/30 to-transparent" }), _jsx("span", { className: "absolute right-1.5 top-[-2px] text-[10px] font-semibold tabular-nums text-success-700", children: formatClock(now, { tz }) })] }));
6
+ return (_jsxs("div", { role: "status", "aria-live": "polite", className: cn('relative h-0 pointer-events-none', className), children: [_jsx("span", { className: "pointer-events-auto absolute left-0 top-0 inline-flex items-center rounded-md bg-success-700 px-2.5 py-1 text-[11px] font-bold uppercase tracking-wider text-white shadow-[0_6px_18px_rgba(16,185,129,0.35)]", children: "LIVE" }), _jsx("span", { "aria-hidden": "true", className: "absolute right-0 top-1/2 left-[60px] h-px bg-gradient-to-r from-success via-success/30 to-transparent" }), _jsx("span", { "data-chromatic": "ignore", className: "absolute right-1.5 top-[-2px] text-[10px] font-semibold tabular-nums text-success-700", children: formatClock(now, { tz }) })] }));
7
7
  }
@@ -34,7 +34,7 @@ export function ScrubBar({ durationMs, positionMs, isLive = false, marks, onSeek
34
34
  onSeek(Math.min(durationMs, positionMs + step));
35
35
  }
36
36
  };
37
- return (_jsxs("div", { className: cn('rounded-2xl border border-border bg-card p-3.5', className), children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground", children: "Timeline \u00B7 this call" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("span", { className: "px-1.5 text-[11px] tabular-nums text-muted-foreground", children: [formatDuration(positionMs), isLive ? ' / live' : ` / ${formatDuration(durationMs)}`] }), onSkipBack && (_jsx(TransportBtn, { label: "Skip back", onClick: onSkipBack, children: _jsx(SkipBack, { className: "size-3.5" }) })), showPlayPause && (_jsx(TransportBtn, { label: isPlayingValue ? 'Pause' : 'Play', variant: "primary", onClick: onPlayPause ?? (() => { }), children: isPlayingValue ? _jsx(Pause, { className: "size-3.5" }) : _jsx(Play, { className: "size-3.5" }) })), onSkipForward && (_jsx(TransportBtn, { label: "Skip forward", onClick: onSkipForward, children: _jsx(SkipForward, { className: "size-3.5" }) }))] })] }), _jsxs("div", { ref: trackRef, role: "slider", "aria-label": "Conversation position", "aria-valuemin": 0, "aria-valuemax": durationMs, "aria-valuenow": positionMs, onClick: handleSeek, onKeyDown: handleKeyDown, tabIndex: 0, className: "relative h-8 cursor-pointer overflow-hidden rounded-md bg-muted focus:outline-none focus:ring-2 focus:ring-primary/50", children: [_jsx("span", { "aria-hidden": "true", className: "absolute inset-y-0 left-0 bg-primary/10", style: { width: `${fillPct}%` } }), marks.map((m) => (_jsx("span", { "data-mark-kind": m.kind, title: m.label ?? m.kind, className: cn('absolute inset-y-1 w-0.5 rounded-sm', KIND_COLOR[m.kind]), style: { left: `${m.positionPct}%` } }, m.id))), _jsx("span", { "aria-hidden": "true", className: "absolute -inset-y-0.5 w-0.5 bg-foreground", style: { left: `${fillPct}%` } })] }), _jsxs("div", { className: "flex justify-between pt-2 text-[10px] tabular-nums text-muted-foreground", children: [_jsx("span", { children: "0:00" }), _jsx("span", { children: isLive ? 'now' : formatDuration(durationMs) })] })] }));
37
+ return (_jsxs("div", { className: cn('rounded-2xl border border-border bg-card p-3.5', className), children: [_jsxs("div", { className: "mb-2.5 flex items-center justify-between", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground", children: "Timeline \u00B7 this call" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("span", { className: "px-1.5 text-[11px] tabular-nums text-muted-foreground", children: [formatDuration(positionMs), isLive ? ' / live' : ` / ${formatDuration(durationMs)}`] }), onSkipBack && (_jsx(TransportBtn, { label: "Skip back", onClick: onSkipBack, children: _jsx(SkipBack, { className: "size-3.5" }) })), showPlayPause && (_jsx(TransportBtn, { label: isPlayingValue ? 'Pause' : 'Play', variant: "primary", onClick: onPlayPause ?? (() => { }), children: isPlayingValue ? _jsx(Pause, { className: "size-3.5" }) : _jsx(Play, { className: "size-3.5" }) })), onSkipForward && (_jsx(TransportBtn, { label: "Skip forward", onClick: onSkipForward, children: _jsx(SkipForward, { className: "size-3.5" }) }))] })] }), _jsxs("div", { ref: trackRef, role: "slider", "aria-label": "Conversation position", "aria-valuemin": 0, "aria-valuemax": durationMs, "aria-valuenow": positionMs, onClick: handleSeek, onKeyDown: handleKeyDown, tabIndex: 0, className: "relative h-8 cursor-pointer overflow-hidden rounded-md bg-muted focus:outline-none focus:ring-2 focus:ring-primary/50", children: [_jsx("span", { "aria-hidden": "true", className: "absolute inset-y-0 left-0 bg-primary/10", style: { width: `${fillPct}%` } }), marks.map((m) => (_jsx("span", { "data-mark-kind": m.kind, title: m.label ?? m.kind, className: cn('absolute inset-y-1 w-0.5 rounded-sm', KIND_COLOR[m.kind]), style: { left: `${m.positionPct}%` } }, m.id))), _jsx("span", { "aria-hidden": "true", "data-chromatic": "ignore", className: "absolute -inset-y-0.5 w-0.5 bg-foreground", style: { left: `${fillPct}%` } })] }), _jsxs("div", { className: "flex justify-between pt-2 text-[10px] tabular-nums text-muted-foreground", children: [_jsx("span", { children: "0:00" }), _jsx("span", { children: isLive ? 'now' : formatDuration(durationMs) })] })] }));
38
38
  }
39
39
  function TransportBtn({ label, onClick, variant, children, }) {
40
40
  return (_jsx("button", { type: "button", "aria-label": label, onClick: onClick, className: cn('inline-flex size-6 items-center justify-center rounded', variant === 'primary'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -14,6 +14,9 @@
14
14
  "type": "module",
15
15
  "main": "./dist/index.js",
16
16
  "types": "./dist/index.d.ts",
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
17
20
  "files": [
18
21
  "README.md",
19
22
  "components.json",
@@ -158,6 +161,7 @@
158
161
  "release": "pnpm build && changeset publish",
159
162
  "registry:validate": "node ./registry/scripts/validate-registry.mjs",
160
163
  "lint:tokens": "node scripts/lint-tokens.mjs",
164
+ "check:stories": "node scripts/check-stories.mjs",
161
165
  "quality": "pnpm lint && pnpm typecheck && pnpm lint:tokens && pnpm test"
162
166
  }
163
167
  }
@@ -1,22 +0,0 @@
1
- 'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { ChevronLeft } from 'lucide-react';
4
- import { cn } from '../../lib/utils.js';
5
- import { useMedaShell } from '../shell-provider.js';
6
- import { useShellViewport } from '../use-shell-viewport.js';
7
- /**
8
- * Mobile-only header.
9
- *
10
- * Root mode: workspace name + optional globalActions.
11
- * Nested mode (when parentLabel is set): ← parentLabel · pageTitle back button.
12
- *
13
- * Renders nothing on non-mobile viewports.
14
- */
15
- export function MobileHeader({ parentLabel, parentTo: _parentTo, title, onBack, globalActions, className, }) {
16
- const ctx = useMedaShell();
17
- const band = useShellViewport();
18
- const isNested = !!parentLabel;
19
- if (band !== 'mobile')
20
- return null;
21
- return (_jsx("header", { "data-meda-mobile-header": isNested ? 'nested' : 'root', className: cn('flex h-[var(--shell-header-height)] items-center justify-between border-b border-border bg-card px-3', className), children: isNested ? (_jsxs("button", { type: "button", onClick: onBack, "aria-label": "Go back", className: "flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm hover:bg-accent", children: [_jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "text-muted-foreground", children: parentLabel }), title && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground/40", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "text-foreground", children: title })] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [_jsx("span", { "aria-hidden": "true", children: ctx.workspace.icon }), _jsx("span", { children: ctx.workspace.name })] }), globalActions && _jsx("div", { className: "flex items-center gap-1", children: globalActions })] })) }));
22
- }
@@ -1,4 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- export declare function NavigationArea({ children }: {
3
- children: ReactNode;
4
- }): import("react/jsx-runtime").JSX.Element;