@medalsocial/meda 0.3.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.
- package/README.md +26 -0
- package/dist/brand/index.d.ts +2 -0
- package/dist/brand/index.js +1 -0
- package/dist/brand/medal-social-mark.d.ts +7 -0
- package/dist/brand/medal-social-mark.js +6 -0
- package/dist/brand/public.d.ts +2 -0
- package/dist/brand/public.js +1 -0
- package/dist/chat/latency-badge.js +7 -3
- package/dist/chat/latency-breakdown.js +1 -1
- package/dist/chat/tool-call-block.js +1 -1
- package/dist/chat/turn-card.js +4 -2
- package/dist/components/ui/command.d.ts +19 -0
- package/dist/components/ui/command.js +37 -0
- package/dist/components/ui/dialog.d.ts +26 -0
- package/dist/components/ui/dialog.js +39 -0
- package/dist/components/ui/drawer.d.ts +16 -0
- package/dist/components/ui/drawer.js +35 -0
- package/dist/components/ui/dropdown-menu.d.ts +32 -0
- package/dist/components/ui/dropdown-menu.js +68 -0
- package/dist/components/ui/tooltip.d.ts +6 -0
- package/dist/components/ui/tooltip.js +17 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/marketing/index.d.ts +5 -0
- package/dist/marketing/index.js +4 -0
- package/dist/marketing/marketing-callout.d.ts +2 -0
- package/dist/marketing/marketing-callout.js +8 -0
- package/dist/marketing/marketing-contact.d.ts +2 -0
- package/dist/marketing/marketing-contact.js +19 -0
- package/dist/marketing/marketing-cta-list.d.ts +2 -0
- package/dist/marketing/marketing-cta-list.js +21 -0
- package/dist/marketing/marketing-lead-magnet.d.ts +2 -0
- package/dist/marketing/marketing-lead-magnet.js +9 -0
- package/dist/marketing/public.d.ts +1 -0
- package/dist/marketing/public.js +1 -0
- package/dist/marketing/types.d.ts +67 -0
- package/dist/marketing/types.js +1 -0
- package/dist/marketing/utils.d.ts +2 -0
- package/dist/marketing/utils.js +6 -0
- package/dist/panel/inspector-json.js +5 -3
- package/dist/shell/app-shell-auth.d.ts +5 -0
- package/dist/shell/app-shell-auth.js +50 -0
- package/dist/shell/app-shell-chat.d.ts +6 -0
- package/dist/shell/app-shell-chat.js +9 -0
- package/dist/shell/app-shell-workspace.d.ts +10 -0
- package/dist/shell/app-shell-workspace.js +55 -0
- package/dist/shell/app-shell.d.ts +25 -0
- package/dist/shell/app-shell.js +26 -0
- package/dist/shell/command-palette.d.ts +23 -0
- package/dist/shell/command-palette.js +138 -0
- package/dist/shell/context-rail.d.ts +31 -0
- package/dist/shell/context-rail.js +81 -0
- package/dist/shell/extras/index.d.ts +4 -0
- package/dist/shell/extras/index.js +4 -0
- package/dist/shell/{shell-desktop-panel-dock.js → extras/shell-desktop-panel-dock.js} +1 -1
- package/dist/shell/extras/types.d.ts +64 -0
- package/dist/shell/extras/types.js +2 -0
- package/dist/shell/icon-rail.d.ts +29 -0
- package/dist/shell/icon-rail.js +38 -0
- package/dist/shell/index.d.ts +19 -15
- package/dist/shell/index.js +30 -15
- package/dist/shell/internal/mobile-bottom-nav.d.ts +23 -0
- package/dist/shell/internal/mobile-bottom-nav.js +79 -0
- package/dist/shell/internal/mobile-drawers.d.ts +26 -0
- package/dist/shell/internal/mobile-drawers.js +76 -0
- package/dist/shell/internal/mobile-header.d.ts +23 -0
- package/dist/shell/internal/mobile-header.js +22 -0
- package/dist/shell/layout-state.d.ts +32 -0
- package/dist/shell/layout-state.js +98 -0
- package/dist/shell/motion.d.ts +22 -0
- package/dist/shell/motion.js +19 -0
- package/dist/shell/resizable-shell.d.ts +45 -0
- package/dist/shell/resizable-shell.js +15 -0
- package/dist/shell/right-panel.d.ts +15 -0
- package/dist/shell/right-panel.js +126 -0
- package/dist/shell/shell-header.d.ts +10 -9
- package/dist/shell/shell-header.js +57 -14
- package/dist/shell/shell-layout-utils.d.ts +2 -2
- package/dist/shell/shell-main.d.ts +8 -0
- package/dist/shell/shell-main.js +11 -0
- package/dist/shell/shell-provider.d.ts +65 -0
- package/dist/shell/shell-provider.js +152 -0
- package/dist/shell/shell-route-utils.d.ts +1 -1
- package/dist/shell/{navigation-area.d.ts → theme-next-themes.d.ts} +1 -1
- package/dist/shell/theme-next-themes.js +24 -0
- package/dist/shell/theme.d.ts +9 -0
- package/dist/shell/theme.js +58 -0
- package/dist/shell/types.d.ts +73 -38
- package/dist/shell/types.js +1 -0
- package/dist/shell/use-shell-viewport.d.ts +2 -0
- package/dist/shell/use-shell-viewport.js +40 -0
- package/dist/shell/utils.d.ts +1 -1
- package/dist/{styles.css → styles/globals.css} +2 -2
- package/dist/styles/theme.css +147 -0
- package/dist/styles/tokens.css +338 -0
- package/dist/timeline/date-switcher.js +3 -1
- package/dist/timeline/event-card.js +5 -13
- package/dist/timeline/live-indicator.js +3 -1
- package/dist/timeline/scrub-bar.js +8 -9
- package/dist/timeline/timeline-rail.js +6 -4
- package/dist/timeline/timeline-tape.js +7 -8
- package/dist/voice/voice-level.js +3 -1
- package/dist/voice/voice-orb-scene.js +1 -0
- package/dist/voice/voice-orb.js +9 -11
- package/dist/voice/voice-status-pill.js +4 -9
- package/package.json +57 -6
- package/dist/shell/navigation-area.js +0 -4
- package/dist/shell/public.d.ts +0 -2
- package/dist/shell/public.js +0 -1
- package/dist/shell/shell-app-rail.d.ts +0 -18
- package/dist/shell/shell-app-rail.js +0 -27
- package/dist/shell/shell-frame.d.ts +0 -13
- package/dist/shell/shell-frame.js +0 -4
- package/dist/shell/shell-module-nav.d.ts +0 -29
- package/dist/shell/shell-module-nav.js +0 -16
- package/dist/shell/shell-panel-rail.d.ts +0 -9
- package/dist/shell/shell-panel-rail.js +0 -17
- package/dist/shell/shell-state.d.ts +0 -39
- package/dist/shell/shell-state.js +0 -135
- /package/dist/shell/{shell-desktop-panel-dock.d.ts → extras/shell-desktop-panel-dock.d.ts} +0 -0
- /package/dist/shell/{shell-scrollable-content.d.ts → extras/shell-scrollable-content.d.ts} +0 -0
- /package/dist/shell/{shell-scrollable-content.js → extras/shell-scrollable-content.js} +0 -0
- /package/dist/shell/{shell-tab-bar.d.ts → extras/shell-tab-bar.d.ts} +0 -0
- /package/dist/shell/{shell-tab-bar.js → extras/shell-tab-bar.js} +0 -0
- /package/dist/shell/{workbench-layout.d.ts → extras/workbench-layout.d.ts} +0 -0
- /package/dist/shell/{workbench-layout.js → extras/workbench-layout.js} +0 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { MarketingLeadMagnetProps } from './types.js';
|
|
2
|
+
export declare function MarketingLeadMagnet({ title, description, benefits, image, buttonText, formTitle, form, variant, open, defaultOpen, onOpenChange, className, }: MarketingLeadMagnetProps): 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 { Dialog } from '@base-ui/react/dialog';
|
|
4
|
+
import { CheckCircle2, X } from 'lucide-react';
|
|
5
|
+
import { cx } from './utils.js';
|
|
6
|
+
export function MarketingLeadMagnet({ title, description, benefits = [], image, buttonText = 'Download now', formTitle = 'Get the resource', form, variant = 'featured', open, defaultOpen, onOpenChange, className, }) {
|
|
7
|
+
const isSidebar = variant === 'sidebar';
|
|
8
|
+
return (_jsx("section", { className: cx('overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-sm', isSidebar ? 'p-6' : 'p-6 sm:p-8 lg:p-10', className), children: _jsxs("div", { className: cx('grid gap-8', isSidebar ? 'grid-cols-1' : 'lg:grid-cols-[0.8fr_1fr]'), children: [!isSidebar && image && _jsx("div", { className: "overflow-hidden rounded-lg bg-muted", children: image }), _jsxs("div", { className: "flex min-w-0 flex-col items-start justify-center", children: [_jsx("h2", { className: "text-2xl font-bold leading-tight text-foreground sm:text-3xl", children: title }), description && (_jsx("p", { className: "mt-3 text-base leading-7 text-muted-foreground", children: description })), benefits.length > 0 && (_jsx("ul", { className: "mt-5 space-y-3", children: benefits.map((benefit, index) => (_jsxs("li", { className: "flex items-start gap-3 text-sm leading-6 text-muted-foreground", children: [_jsx(CheckCircle2, { className: "mt-0.5 size-4 shrink-0 text-primary", "aria-hidden": true }), _jsx("span", { children: benefit })] }, typeof benefit === 'string' ? `${benefit}-${index}` : `benefit-${index}`))) })), _jsxs(Dialog.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: (nextOpen) => onOpenChange?.(nextOpen), children: [_jsx(Dialog.Trigger, { className: "mt-6 inline-flex min-h-10 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", children: buttonText }), _jsxs(Dialog.Portal, { children: [_jsx(Dialog.Backdrop, { className: "fixed inset-0 z-modal bg-background/80 backdrop-blur-sm" }), _jsxs(Dialog.Popup, { className: "fixed left-1/2 top-1/2 z-modal w-[calc(100vw-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-lg border border-border bg-card text-card-foreground shadow-lg focus-visible:outline-none", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 border-b border-border px-6 py-5", children: [_jsx(Dialog.Title, { className: "text-lg font-semibold text-foreground", children: formTitle }), _jsxs(Dialog.Close, { className: "inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx("span", { className: "sr-only", children: "Close" }), _jsx(X, { className: "size-4", "aria-hidden": true })] })] }), _jsx("div", { className: "p-6", children: form })] })] })] })] })] }) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { MarketingCalloutProps, MarketingContactPerson, MarketingContactProps, MarketingCta, MarketingLeadMagnetProps, MarketingOffice, } from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MouseEventHandler, ReactNode } from 'react';
|
|
2
|
+
export type MarketingCtaVariant = 'primary' | 'secondary';
|
|
3
|
+
export type MarketingAlign = 'start' | 'center';
|
|
4
|
+
export interface MarketingCta {
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
href?: string;
|
|
7
|
+
onClick?: MouseEventHandler<HTMLButtonElement>;
|
|
8
|
+
variant?: MarketingCtaVariant;
|
|
9
|
+
ariaLabel?: string;
|
|
10
|
+
target?: string;
|
|
11
|
+
rel?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface MarketingCtaListProps {
|
|
14
|
+
ctas?: MarketingCta[];
|
|
15
|
+
align?: MarketingAlign;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
export type MarketingCalloutVariant = 'band' | 'card';
|
|
19
|
+
export interface MarketingCalloutProps {
|
|
20
|
+
eyebrow?: ReactNode;
|
|
21
|
+
title: ReactNode;
|
|
22
|
+
description?: ReactNode;
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
ctas?: MarketingCta[];
|
|
25
|
+
variant?: MarketingCalloutVariant;
|
|
26
|
+
align?: MarketingAlign;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface MarketingOffice {
|
|
30
|
+
title?: ReactNode;
|
|
31
|
+
address?: ReactNode;
|
|
32
|
+
email?: string;
|
|
33
|
+
phone?: string;
|
|
34
|
+
hours?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
export interface MarketingContactPerson {
|
|
37
|
+
title?: ReactNode;
|
|
38
|
+
name: ReactNode;
|
|
39
|
+
role?: ReactNode;
|
|
40
|
+
description?: ReactNode;
|
|
41
|
+
image?: ReactNode;
|
|
42
|
+
email?: string;
|
|
43
|
+
phone?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface MarketingContactProps {
|
|
46
|
+
intro?: ReactNode;
|
|
47
|
+
form: ReactNode;
|
|
48
|
+
office?: MarketingOffice;
|
|
49
|
+
contactPerson?: MarketingContactPerson;
|
|
50
|
+
compact?: boolean;
|
|
51
|
+
className?: string;
|
|
52
|
+
}
|
|
53
|
+
export type MarketingLeadMagnetVariant = 'featured' | 'sidebar';
|
|
54
|
+
export interface MarketingLeadMagnetProps {
|
|
55
|
+
title: ReactNode;
|
|
56
|
+
description?: ReactNode;
|
|
57
|
+
benefits?: ReactNode[];
|
|
58
|
+
image?: ReactNode;
|
|
59
|
+
buttonText?: ReactNode;
|
|
60
|
+
formTitle?: ReactNode;
|
|
61
|
+
form?: ReactNode;
|
|
62
|
+
variant?: MarketingLeadMagnetVariant;
|
|
63
|
+
open?: boolean;
|
|
64
|
+
defaultOpen?: boolean;
|
|
65
|
+
onOpenChange?: (open: boolean) => void;
|
|
66
|
+
className?: string;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -57,12 +57,14 @@ function tokenize(value, level, key, indent, seen) {
|
|
|
57
57
|
}
|
|
58
58
|
return out;
|
|
59
59
|
}
|
|
60
|
+
// 700-shade variants for syntax tokens so 11px monospace text clears
|
|
61
|
+
// WCAG AA (4.5:1) against the inspector's near-white background.
|
|
60
62
|
const KIND_CLASS = {
|
|
61
63
|
punct: 'text-muted-foreground',
|
|
62
64
|
key: 'text-primary',
|
|
63
|
-
string: 'text-emerald-
|
|
64
|
-
number: 'text-amber-
|
|
65
|
-
literal: 'text-amber-
|
|
65
|
+
string: 'text-emerald-700',
|
|
66
|
+
number: 'text-amber-700',
|
|
67
|
+
literal: 'text-amber-700',
|
|
66
68
|
};
|
|
67
69
|
export function InspectorJSON({ data, indent = 2, className }) {
|
|
68
70
|
const tokens = tokenize(data, 0, null, indent, new WeakSet());
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
|
|
3
|
+
interface AppShellBaseProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className?: string;
|
|
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;
|
|
21
|
+
export declare function AppShellBody({ children, className }: {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
className?: string;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
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';
|
|
7
|
+
import { useMedaShell } from './shell-provider.js';
|
|
8
|
+
export function AppShell(props) {
|
|
9
|
+
const { workspace, activeAppId } = useMedaShell();
|
|
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
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function AppShellBody({ children, className }) {
|
|
25
|
+
return (_jsx("div", { className: cn('relative flex h-[calc(100vh-var(--shell-header-height))] overflow-hidden', className), children: children }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { CommandDefinition } from './types.js';
|
|
3
|
+
export interface CommandGroupDefinition {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Lower value = rendered earlier. Default: 100. */
|
|
7
|
+
priority?: number;
|
|
8
|
+
}
|
|
9
|
+
interface CommandRegistry {
|
|
10
|
+
register: (commands: CommandDefinition[]) => void;
|
|
11
|
+
unregister: (ids: string[]) => void;
|
|
12
|
+
registerGroup: (group: CommandGroupDefinition) => void;
|
|
13
|
+
unregisterGroup: (id: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare const CommandRegistryContext: import("react").Context<CommandRegistry | null>;
|
|
16
|
+
export declare function matchesHotkey(e: KeyboardEvent, spec: string): boolean;
|
|
17
|
+
export interface CommandPaletteProps {
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function CommandPalette({ children }: CommandPaletteProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function useCommands(commands: CommandDefinition[]): void;
|
|
22
|
+
export declare function useCommandGroup(group: CommandGroupDefinition): void;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandShortcut, } from '../components/ui/command.js';
|
|
5
|
+
import { useMedaShell } from './shell-provider.js';
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Context
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
export const CommandRegistryContext = createContext(null);
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// matchesHotkey — strict modifier matching
|
|
12
|
+
//
|
|
13
|
+
// 'mod+k' → matches Cmd+K on macOS, Ctrl+K on Windows/Linux
|
|
14
|
+
// 'mod+shift+k' → requires modifier + shift + key; no other modifiers
|
|
15
|
+
// 'mod+alt+k' → requires modifier + alt + key; no other modifiers
|
|
16
|
+
//
|
|
17
|
+
// Modifiers not listed in `spec` MUST NOT be pressed.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export function matchesHotkey(e, spec) {
|
|
20
|
+
const parts = spec.toLowerCase().split('+');
|
|
21
|
+
const key = parts.at(-1) ?? '';
|
|
22
|
+
const mods = parts.slice(0, -1);
|
|
23
|
+
const wantMod = mods.includes('mod');
|
|
24
|
+
const wantShift = mods.includes('shift');
|
|
25
|
+
const wantAlt = mods.includes('alt');
|
|
26
|
+
const hasMod = e.metaKey || e.ctrlKey;
|
|
27
|
+
return (e.key.toLowerCase() === key &&
|
|
28
|
+
(wantMod ? hasMod : !hasMod) &&
|
|
29
|
+
(wantShift ? e.shiftKey : !e.shiftKey) &&
|
|
30
|
+
(wantAlt ? e.altKey : !e.altKey));
|
|
31
|
+
}
|
|
32
|
+
export function CommandPalette({ children }) {
|
|
33
|
+
const ctx = useMedaShell();
|
|
34
|
+
const { open, setOpen } = ctx.commandPalette;
|
|
35
|
+
const hotkey = ctx.commandPaletteHotkey;
|
|
36
|
+
const [commands, setCommands] = useState([]);
|
|
37
|
+
const [groups, setGroups] = useState([]);
|
|
38
|
+
// -------------------------------------------------------------------------
|
|
39
|
+
// Registry methods
|
|
40
|
+
// -------------------------------------------------------------------------
|
|
41
|
+
const register = useCallback((cmds) => {
|
|
42
|
+
setCommands((prev) => {
|
|
43
|
+
const ids = new Set(cmds.map((c) => c.id));
|
|
44
|
+
return [...prev.filter((c) => !ids.has(c.id)), ...cmds];
|
|
45
|
+
});
|
|
46
|
+
}, []);
|
|
47
|
+
const unregister = useCallback((ids) => {
|
|
48
|
+
setCommands((prev) => prev.filter((c) => !ids.includes(c.id)));
|
|
49
|
+
}, []);
|
|
50
|
+
const registerGroup = useCallback((group) => {
|
|
51
|
+
setGroups((prev) => [...prev.filter((g) => g.id !== group.id), group]);
|
|
52
|
+
}, []);
|
|
53
|
+
const unregisterGroup = useCallback((id) => {
|
|
54
|
+
setGroups((prev) => prev.filter((g) => g.id !== id));
|
|
55
|
+
}, []);
|
|
56
|
+
const registry = useMemo(() => ({ register, unregister, registerGroup, unregisterGroup }), [register, unregister, registerGroup, unregisterGroup]);
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// Hotkey listener
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const onKey = (e) => {
|
|
62
|
+
if (matchesHotkey(e, hotkey)) {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
setOpen(true);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
window.addEventListener('keydown', onKey);
|
|
68
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
69
|
+
}, [hotkey, setOpen]);
|
|
70
|
+
// -------------------------------------------------------------------------
|
|
71
|
+
// Group + sort commands
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
const grouped = useMemo(() => {
|
|
74
|
+
const byGroup = new Map();
|
|
75
|
+
for (const cmd of commands) {
|
|
76
|
+
const list = byGroup.get(cmd.group) ?? [];
|
|
77
|
+
list.push(cmd);
|
|
78
|
+
byGroup.set(cmd.group, list);
|
|
79
|
+
}
|
|
80
|
+
const orderedGroupIds = [...byGroup.keys()].sort((a, b) => {
|
|
81
|
+
const ga = groups.find((g) => g.id === a);
|
|
82
|
+
const gb = groups.find((g) => g.id === b);
|
|
83
|
+
const pa = ga?.priority ?? 100;
|
|
84
|
+
const pb = gb?.priority ?? 100;
|
|
85
|
+
if (pa !== pb)
|
|
86
|
+
return pa - pb;
|
|
87
|
+
const la = ga?.label ?? a;
|
|
88
|
+
const lb = gb?.label ?? b;
|
|
89
|
+
return la.localeCompare(lb);
|
|
90
|
+
});
|
|
91
|
+
return orderedGroupIds.map((groupId) => {
|
|
92
|
+
const def = groups.find((g) => g.id === groupId);
|
|
93
|
+
return {
|
|
94
|
+
id: groupId,
|
|
95
|
+
label: def?.label ?? groupId,
|
|
96
|
+
commands: (byGroup.get(groupId) ?? []).sort((a, b) => a.label.localeCompare(b.label)),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}, [commands, groups]);
|
|
100
|
+
// -------------------------------------------------------------------------
|
|
101
|
+
// Handlers
|
|
102
|
+
// -------------------------------------------------------------------------
|
|
103
|
+
const handleSelect = (cmd) => {
|
|
104
|
+
setOpen(false);
|
|
105
|
+
void cmd.run();
|
|
106
|
+
};
|
|
107
|
+
// -------------------------------------------------------------------------
|
|
108
|
+
// Render
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
return (_jsxs(CommandRegistryContext.Provider, { value: registry, children: [children, _jsx(CommandDialog, { open: open, onOpenChange: setOpen, children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Search commands\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No results." }), grouped.map((group) => (_jsx(CommandGroup, { heading: group.label, children: group.commands.map((cmd) => {
|
|
111
|
+
const Icon = cmd.icon;
|
|
112
|
+
return (_jsxs(CommandItem, { value: `${group.label} ${cmd.label}`, onSelect: () => handleSelect(cmd), children: [Icon && _jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: cmd.label }), cmd.shortcut && _jsx(CommandShortcut, { children: cmd.shortcut })] }, cmd.id));
|
|
113
|
+
}) }, group.id)))] })] }) })] }));
|
|
114
|
+
}
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// useCommands — registers on mount, unregisters on unmount
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
export function useCommands(commands) {
|
|
119
|
+
const registry = useContext(CommandRegistryContext);
|
|
120
|
+
if (!registry)
|
|
121
|
+
throw new Error('useCommands must be used inside <CommandPalette>');
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
registry.register(commands);
|
|
124
|
+
return () => registry.unregister(commands.map((c) => c.id));
|
|
125
|
+
}, [commands, registry.register, registry.unregister]);
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// useCommandGroup — registers group metadata for ordering
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
export function useCommandGroup(group) {
|
|
131
|
+
const registry = useContext(CommandRegistryContext);
|
|
132
|
+
if (!registry)
|
|
133
|
+
throw new Error('useCommandGroup must be used inside <CommandPalette>');
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
registry.registerGroup(group);
|
|
136
|
+
return () => registry.unregisterGroup(group.id);
|
|
137
|
+
}, [group, registry.registerGroup, registry.unregisterGroup]);
|
|
138
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextRail — spec §10
|
|
3
|
+
*
|
|
4
|
+
* Renders a resizable, collapsible, persisted context navigation rail.
|
|
5
|
+
* Width is stored per-(workspaceId, appId) via useShellLayoutState through
|
|
6
|
+
* the MedaShellProvider context.
|
|
7
|
+
*
|
|
8
|
+
* Resize integration note (Phase 9 / Pattern B):
|
|
9
|
+
* The rail uses its own pointer-events resize handle on the right edge instead
|
|
10
|
+
* of wrapping in <ResizableShell> (Pattern A). Pattern A requires migrating
|
|
11
|
+
* <AppShellBody> to a PanelGroup layout — that is Phase 11 territory.
|
|
12
|
+
* TODO(phase-11): replace pointer-events handle with <ResizableShellPanel>
|
|
13
|
+
* once <AppShellBody> ships as a ResizableShell Group.
|
|
14
|
+
*/
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
16
|
+
import type { ContextModule, ShellLinkRenderArgs } from './types.js';
|
|
17
|
+
export interface ContextRailProps {
|
|
18
|
+
/** Drives the persistence key for layout state. */
|
|
19
|
+
appId: string;
|
|
20
|
+
module?: ContextModule;
|
|
21
|
+
/** Starting width in px. Falls back to provider's stored width (default 300). */
|
|
22
|
+
defaultWidth?: number;
|
|
23
|
+
collapsible?: boolean;
|
|
24
|
+
/** When true, the rail evaporates — renders an aria-hidden placeholder. */
|
|
25
|
+
hidden?: boolean;
|
|
26
|
+
/** Optional active item id for nav active state (preferred over useMedaShell().selection). */
|
|
27
|
+
activeItemId?: string;
|
|
28
|
+
renderLink?: (args: ShellLinkRenderArgs) => ReactNode;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function ContextRail({ appId: _appId, module, hidden, collapsible: _collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState } from '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
|
+
// Constants — spec §10 dimensions
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const MIN_WIDTH = 240;
|
|
11
|
+
const MAX_WIDTH = 420;
|
|
12
|
+
function ResizeHandle({ currentWidth, onResize, onCommit }) {
|
|
13
|
+
const startWidthRef = useRef(currentWidth);
|
|
14
|
+
const startXRef = useRef(0);
|
|
15
|
+
const draggingRef = useRef(false);
|
|
16
|
+
const handlePointerDown = (e) => {
|
|
17
|
+
// setPointerCapture may not be available in all test environments (jsdom)
|
|
18
|
+
try {
|
|
19
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// ignore — drag still functions via draggingRef
|
|
23
|
+
}
|
|
24
|
+
startWidthRef.current = currentWidth;
|
|
25
|
+
startXRef.current = e.clientX;
|
|
26
|
+
draggingRef.current = true;
|
|
27
|
+
};
|
|
28
|
+
const handlePointerMove = (e) => {
|
|
29
|
+
if (!draggingRef.current)
|
|
30
|
+
return;
|
|
31
|
+
const next = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + (e.clientX - startXRef.current)));
|
|
32
|
+
onResize(next);
|
|
33
|
+
};
|
|
34
|
+
const handlePointerUp = (e) => {
|
|
35
|
+
if (!draggingRef.current)
|
|
36
|
+
return;
|
|
37
|
+
draggingRef.current = false;
|
|
38
|
+
const next = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + (e.clientX - startXRef.current)));
|
|
39
|
+
onCommit(next);
|
|
40
|
+
};
|
|
41
|
+
return (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize context rail", "aria-valuenow": currentWidth, "aria-valuemin": MIN_WIDTH, "aria-valuemax": MAX_WIDTH, tabIndex: 0, className: cn('absolute top-0 right-0 h-full w-1 cursor-col-resize', 'opacity-0 transition-opacity hover:opacity-100 hover:bg-ring'), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp }));
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// ContextRail
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
export function ContextRail({ appId: _appId, module, hidden = false, collapsible: _collapsible = true, activeItemId, renderLink, className, }) {
|
|
47
|
+
const band = useShellViewport();
|
|
48
|
+
const ctx = useMedaShell();
|
|
49
|
+
const collapsed = ctx.contextRail.collapsed;
|
|
50
|
+
// Local display width tracks pointer-move updates; ctx.contextRail.setWidth
|
|
51
|
+
// is called on pointerUp to persist via useShellLayoutState.
|
|
52
|
+
const [displayWidth, setDisplayWidth] = useState(null);
|
|
53
|
+
const width = displayWidth ?? ctx.contextRail.width;
|
|
54
|
+
if (band === 'mobile')
|
|
55
|
+
return null;
|
|
56
|
+
if (hidden) {
|
|
57
|
+
return _jsx("div", { "aria-hidden": "true", className: "hidden", "data-testid": "context-rail-hidden" });
|
|
58
|
+
}
|
|
59
|
+
if (!module || module.items.length === 0) {
|
|
60
|
+
return _jsx("div", { "aria-hidden": "true", className: "hidden", "data-testid": "context-rail-empty" });
|
|
61
|
+
}
|
|
62
|
+
const handleResize = (w) => {
|
|
63
|
+
setDisplayWidth(w);
|
|
64
|
+
};
|
|
65
|
+
const handleCommit = (w) => {
|
|
66
|
+
setDisplayWidth(null);
|
|
67
|
+
ctx.contextRail.setWidth(w);
|
|
68
|
+
};
|
|
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
|
+
const isActive = item.id === activeItemId;
|
|
71
|
+
const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
|
|
72
|
+
? 'bg-primary/10 text-primary'
|
|
73
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground');
|
|
74
|
+
const IconComp = item.icon;
|
|
75
|
+
const inner = (_jsxs(_Fragment, { children: [_jsx(IconComp, { size: 16, "aria-hidden": "true", className: "shrink-0" }), _jsx("span", { className: "truncate", children: item.label }), item.shortcut && (_jsx("kbd", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: item.shortcut }))] }));
|
|
76
|
+
if (renderLink) {
|
|
77
|
+
return renderLink({ item, isActive, className: klass, children: inner });
|
|
78
|
+
}
|
|
79
|
+
return (_jsx("a", { href: item.to, "aria-current": isActive ? 'page' : undefined, className: klass, children: inner }, item.id));
|
|
80
|
+
}) }), _jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit })] }));
|
|
81
|
+
}
|