@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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion tokens (spec §14).
|
|
3
|
+
*
|
|
4
|
+
* Numeric values are unitless milliseconds — consumers append `ms`:
|
|
5
|
+
* `transition-duration: ${motion.default}ms`
|
|
6
|
+
*
|
|
7
|
+
* Mirrored as CSS custom properties in `src/styles/tokens.css` for use
|
|
8
|
+
* via Tailwind arbitrary values (e.g. `duration-[var(--motion-default)]`).
|
|
9
|
+
*
|
|
10
|
+
* `spring` is JS-only (no CSS analogue); for Framer Motion / react-spring.
|
|
11
|
+
*/
|
|
12
|
+
export declare const motion: {
|
|
13
|
+
readonly fast: 140;
|
|
14
|
+
readonly default: 200;
|
|
15
|
+
readonly panel: 240;
|
|
16
|
+
readonly fullscreen: 280;
|
|
17
|
+
readonly ease: "cubic-bezier(0.2, 0.8, 0.2, 1)";
|
|
18
|
+
readonly spring: {
|
|
19
|
+
readonly stiffness: 420;
|
|
20
|
+
readonly damping: 34;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion tokens (spec §14).
|
|
3
|
+
*
|
|
4
|
+
* Numeric values are unitless milliseconds — consumers append `ms`:
|
|
5
|
+
* `transition-duration: ${motion.default}ms`
|
|
6
|
+
*
|
|
7
|
+
* Mirrored as CSS custom properties in `src/styles/tokens.css` for use
|
|
8
|
+
* via Tailwind arbitrary values (e.g. `duration-[var(--motion-default)]`).
|
|
9
|
+
*
|
|
10
|
+
* `spring` is JS-only (no CSS analogue); for Framer Motion / react-spring.
|
|
11
|
+
*/
|
|
12
|
+
export const motion = {
|
|
13
|
+
fast: 140,
|
|
14
|
+
default: 200,
|
|
15
|
+
panel: 240,
|
|
16
|
+
fullscreen: 280,
|
|
17
|
+
ease: 'cubic-bezier(0.2, 0.8, 0.2, 1)',
|
|
18
|
+
spring: { stiffness: 420, damping: 34 },
|
|
19
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResizableShell — shadcn-style wrapper around react-resizable-panels v4.
|
|
3
|
+
*
|
|
4
|
+
* API surface: Group → ResizableShell, Panel → ResizableShellPanel,
|
|
5
|
+
* Separator → ResizableHandle.
|
|
6
|
+
*
|
|
7
|
+
* Note: react-resizable-panels v4 renamed the exports:
|
|
8
|
+
* PanelGroup → Group, PanelResizeHandle → Separator
|
|
9
|
+
* The v4 prop for layout direction is `orientation` (not `direction`).
|
|
10
|
+
*
|
|
11
|
+
* Persistence: left intentionally to consumers (ContextRail, RightPanel) because
|
|
12
|
+
* they own the (workspaceId, appId) key needed to key useShellLayoutState.
|
|
13
|
+
* ResizableShell is a layout primitive; don't couple it to the shell context hook.
|
|
14
|
+
*/
|
|
15
|
+
import type { ReactNode } from 'react';
|
|
16
|
+
import type { OnPanelResize } from 'react-resizable-panels';
|
|
17
|
+
export interface ResizableShellProps {
|
|
18
|
+
/** Default: 'horizontal'. */
|
|
19
|
+
orientation?: 'horizontal' | 'vertical';
|
|
20
|
+
className?: string;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export declare function ResizableShell({ orientation, className, children, }: ResizableShellProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export interface ResizableShellPanelProps {
|
|
25
|
+
/** Percent (0–100). */
|
|
26
|
+
defaultSize?: number;
|
|
27
|
+
/** Minimum size in percent. Token-aligned default: none (set by consumer). */
|
|
28
|
+
minSize?: number;
|
|
29
|
+
/** Maximum size in percent. Token-aligned default: none (set by consumer). */
|
|
30
|
+
maxSize?: number;
|
|
31
|
+
id?: string;
|
|
32
|
+
collapsible?: boolean;
|
|
33
|
+
collapsedSize?: number;
|
|
34
|
+
/** Called on each resize. Uses the react-resizable-panels v4 signature. */
|
|
35
|
+
onResize?: OnPanelResize;
|
|
36
|
+
className?: string;
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
}
|
|
39
|
+
export declare function ResizableShellPanel({ className, children, onResize, ...props }: ResizableShellPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export interface ResizableHandleProps {
|
|
41
|
+
className?: string;
|
|
42
|
+
/** When true, renders a visible drag-indicator bar inside the handle. */
|
|
43
|
+
withHandle?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function ResizableHandle({ className, withHandle }: ResizableHandleProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Group, Panel, Separator } from 'react-resizable-panels';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
export function ResizableShell({ orientation = 'horizontal', className, children, }) {
|
|
6
|
+
return (_jsx(Group, { orientation: orientation, className: cn('flex h-full w-full', className), children: children }));
|
|
7
|
+
}
|
|
8
|
+
export function ResizableShellPanel({ className, children, onResize, ...props }) {
|
|
9
|
+
return (_jsx(Panel, { className: cn('h-full', className), onResize: onResize, ...props, children: children }));
|
|
10
|
+
}
|
|
11
|
+
export function ResizableHandle({ className, withHandle = false }) {
|
|
12
|
+
return (_jsx(Separator, { className: cn(
|
|
13
|
+
// 4px wide, transparent until hover/active — then ring-color line
|
|
14
|
+
'group relative w-1 shrink-0 cursor-col-resize transition-colors', 'bg-transparent hover:bg-ring data-[separator]:hover:bg-ring', 'focus-visible:bg-ring focus-visible:outline-none', className), children: withHandle && (_jsx("div", { "data-testid": "resize-handle-indicator", className: cn('absolute top-1/2 left-1/2 h-6 w-1 -translate-x-1/2 -translate-y-1/2', 'rounded-sm bg-border opacity-0 transition-opacity group-hover:opacity-100') })) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PanelMode, PanelView } from './types.js';
|
|
2
|
+
export interface RightPanelProps {
|
|
3
|
+
/** Views to render as tabs in the panel header. */
|
|
4
|
+
panelViews?: PanelView[];
|
|
5
|
+
/** Default active view id — set on mount when no activeView is set in context. */
|
|
6
|
+
defaultView?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Which open-modes this consumer allows.
|
|
9
|
+
* Default is all three: ['panel', 'expanded', 'fullscreen'].
|
|
10
|
+
* If only ['panel'], the cycle button is hidden.
|
|
11
|
+
*/
|
|
12
|
+
modes?: PanelMode[];
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function RightPanel({ panelViews, defaultView, modes, className, }: RightPanelProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* RightPanel — spec §12
|
|
5
|
+
*
|
|
6
|
+
* Four-mode right panel: closed (0px), panel (resizable 300–520px),
|
|
7
|
+
* expanded (~60vw), fullscreen (100vw/100vh takeover).
|
|
8
|
+
*
|
|
9
|
+
* Mode state lives in MedaShellProvider (panel.mode / panel.setMode).
|
|
10
|
+
* Width is persisted per-(workspaceId, appId) via panel.width / panel.setWidth.
|
|
11
|
+
*
|
|
12
|
+
* Resize note (Pattern B):
|
|
13
|
+
* Uses its own pointer-event resize handle on the LEFT edge instead of
|
|
14
|
+
* wrapping in <ResizableShell>. Pattern A requires migrating <AppShellBody>
|
|
15
|
+
* to a PanelGroup layout.
|
|
16
|
+
* TODO(phase-15-refactor): swap to ResizableShell once AppShellBody is a PanelGroup
|
|
17
|
+
*/
|
|
18
|
+
import { Maximize2, Minimize2, X } from 'lucide-react';
|
|
19
|
+
import { useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { cn } from '../lib/utils.js';
|
|
21
|
+
import { useMedaShell } from './shell-provider.js';
|
|
22
|
+
import { useShellViewport } from './use-shell-viewport.js';
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Constants — spec §12 dimensions
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
const MIN_WIDTH = 300;
|
|
27
|
+
const MAX_WIDTH = 520;
|
|
28
|
+
function ResizeHandle({ currentWidth, onResize, onCommit }) {
|
|
29
|
+
const startWidthRef = useRef(currentWidth);
|
|
30
|
+
const startXRef = useRef(0);
|
|
31
|
+
const draggingRef = useRef(false);
|
|
32
|
+
const handlePointerDown = (e) => {
|
|
33
|
+
try {
|
|
34
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// ignore — drag still functions via draggingRef
|
|
38
|
+
}
|
|
39
|
+
startWidthRef.current = currentWidth;
|
|
40
|
+
startXRef.current = e.clientX;
|
|
41
|
+
draggingRef.current = true;
|
|
42
|
+
};
|
|
43
|
+
const handlePointerMove = (e) => {
|
|
44
|
+
if (!draggingRef.current)
|
|
45
|
+
return;
|
|
46
|
+
// Left edge: drag left (clientX decreasing) widens panel
|
|
47
|
+
const delta = startXRef.current - e.clientX;
|
|
48
|
+
const next = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + delta));
|
|
49
|
+
onResize(next);
|
|
50
|
+
};
|
|
51
|
+
const handlePointerUp = (e) => {
|
|
52
|
+
if (!draggingRef.current)
|
|
53
|
+
return;
|
|
54
|
+
draggingRef.current = false;
|
|
55
|
+
const delta = startXRef.current - e.clientX;
|
|
56
|
+
const next = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + delta));
|
|
57
|
+
onCommit(next);
|
|
58
|
+
};
|
|
59
|
+
return (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize panel", "aria-valuenow": currentWidth, "aria-valuemin": MIN_WIDTH, "aria-valuemax": MAX_WIDTH, tabIndex: 0, className: cn('absolute top-0 left-0 h-full w-1 cursor-col-resize', 'opacity-0 transition-opacity hover:opacity-100 hover:bg-ring'), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp }));
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// RightPanel
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
export function RightPanel({ panelViews = [], defaultView, modes = ['panel', 'expanded', 'fullscreen'], className, }) {
|
|
65
|
+
const band = useShellViewport();
|
|
66
|
+
const ctx = useMedaShell();
|
|
67
|
+
const { mode, activeView, width, setMode, setActiveView, setWidth } = ctx.panel;
|
|
68
|
+
// Hydrate defaultView on mount if no active view set
|
|
69
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional one-time mount effect
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!activeView && defaultView) {
|
|
72
|
+
setActiveView(defaultView);
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
// Local display width tracks pointer-move updates live;
|
|
76
|
+
// setWidth (persist) is called on pointerUp.
|
|
77
|
+
const [displayWidth, setDisplayWidth] = useState(null);
|
|
78
|
+
const resolvedWidth = displayWidth ?? width;
|
|
79
|
+
// On mobile, the right panel renders as a drawer via <MobileDrawers > PanelsDrawer />.
|
|
80
|
+
// The desktop-shaped <RightPanel> hides; consumers must mount <MobileDrawers> to expose
|
|
81
|
+
// panel views on mobile.
|
|
82
|
+
if (band === 'mobile')
|
|
83
|
+
return null;
|
|
84
|
+
const renderCtx = {
|
|
85
|
+
workspaceId: ctx.workspace.id,
|
|
86
|
+
appId: ctx.activeAppId,
|
|
87
|
+
};
|
|
88
|
+
// Cycle through allowed open modes only (skip 'closed')
|
|
89
|
+
const cycleOpenMode = () => {
|
|
90
|
+
if (mode === 'closed')
|
|
91
|
+
return;
|
|
92
|
+
const openModes = modes.filter((m) => m !== 'closed');
|
|
93
|
+
const idx = openModes.indexOf(mode);
|
|
94
|
+
const next = openModes[(idx + 1) % openModes.length] ?? mode;
|
|
95
|
+
setMode(next);
|
|
96
|
+
};
|
|
97
|
+
// Mode → inline width style
|
|
98
|
+
const widthStyle = (() => {
|
|
99
|
+
switch (mode) {
|
|
100
|
+
case 'closed':
|
|
101
|
+
return { width: 0, pointerEvents: 'none' };
|
|
102
|
+
case 'panel':
|
|
103
|
+
return { width: `${resolvedWidth}px` };
|
|
104
|
+
case 'expanded':
|
|
105
|
+
return { width: '60vw' };
|
|
106
|
+
case 'fullscreen':
|
|
107
|
+
return { width: '100vw' };
|
|
108
|
+
}
|
|
109
|
+
})();
|
|
110
|
+
// Z-index escalation: fullscreen covers header + rails
|
|
111
|
+
const zIndexClass = mode === 'fullscreen' ? 'z-[var(--z-shell-fullscreen)]' : 'z-[var(--z-shell-panel)]';
|
|
112
|
+
const activePanelView = panelViews.find((v) => v.id === activeView);
|
|
113
|
+
const cycleAriaLabel = mode === 'panel' ? 'Expand panel' : mode === 'expanded' ? 'Maximize panel' : 'Restore panel';
|
|
114
|
+
const handleResize = (w) => setDisplayWidth(w);
|
|
115
|
+
const handleCommit = (w) => {
|
|
116
|
+
setDisplayWidth(null);
|
|
117
|
+
setWidth(w);
|
|
118
|
+
};
|
|
119
|
+
return (_jsx("aside", { "data-meda-panel-mode": mode, "aria-hidden": mode === 'closed' ? 'true' : undefined, className: cn('relative h-full shrink-0 overflow-hidden border-l border-shell-border bg-shell-panel', 'transition-[width] ease-[var(--motion-ease)] duration-[var(--motion-panel)]', mode === 'fullscreen' && 'fixed inset-0 h-screen w-screen border-none', zIndexClass, className), style: widthStyle, children: mode !== 'closed' && (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-shell-border px-3 py-2", children: [_jsx("div", { className: "flex items-center gap-1", children: panelViews.map((view) => {
|
|
120
|
+
const isActive = view.id === activeView;
|
|
121
|
+
const Icon = view.icon;
|
|
122
|
+
return (_jsxs("button", { type: "button", "aria-current": isActive ? 'true' : undefined, onClick: () => setActiveView(view.id), className: cn('inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors', isActive
|
|
123
|
+
? 'bg-accent text-accent-foreground'
|
|
124
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground'), children: [_jsx(Icon, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: view.label })] }, view.id));
|
|
125
|
+
}) }), _jsxs("div", { className: "flex items-center gap-1", children: [modes.length > 1 && (_jsx("button", { type: "button", "aria-label": cycleAriaLabel, onClick: cycleOpenMode, className: "inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground", children: mode === 'fullscreen' ? (_jsx(Minimize2, { size: 14, "aria-hidden": "true" })) : (_jsx(Maximize2, { size: 14, "aria-hidden": "true" })) })), _jsx("button", { type: "button", "aria-label": "Close panel", onClick: () => setMode('closed'), className: "inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { size: 14, "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto", children: activePanelView != null ? (activePanelView.render(renderCtx)) : (_jsx("div", { className: "p-4 text-muted-foreground text-sm", children: "No panel view selected" })) }), mode === 'panel' && (_jsx(ResizeHandle, { currentWidth: resolvedWidth, onResize: handleResize, onCommit: handleCommit }))] })) }));
|
|
126
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export interface WorkspaceSwitcherProps {
|
|
3
|
+
workspaceMenuFooter?: ReactNode;
|
|
4
|
+
}
|
|
5
|
+
export declare function WorkspaceSwitcher({ workspaceMenuFooter }?: WorkspaceSwitcherProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function AppTabs(): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function PanelToggle(): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export interface ShellHeaderProps {
|
|
9
|
+
globalActions?: ReactNode;
|
|
6
10
|
className?: string;
|
|
7
|
-
}
|
|
8
|
-
export declare function
|
|
9
|
-
panelOpen: boolean;
|
|
10
|
-
onToggle: () => void;
|
|
11
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
}
|
|
12
|
+
export declare function ShellHeader({ globalActions, className }?: ShellHeaderProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,15 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronDown, Monitor, Moon, PanelRightClose, PanelRightOpen, Sun } from 'lucide-react';
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../components/ui/dropdown-menu.js';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
import { useMedaShell } from './shell-provider.js';
|
|
7
|
+
import { useTheme } from './theme.js';
|
|
8
|
+
import { useShellViewport } from './use-shell-viewport.js';
|
|
9
|
+
const NEXT_THEME = { light: 'dark', dark: 'system', system: 'light' };
|
|
10
|
+
const THEME_ICON = { light: Sun, dark: Moon, system: Monitor };
|
|
11
|
+
const THEME_LABEL = {
|
|
12
|
+
light: 'Switch to dark theme',
|
|
13
|
+
dark: 'Switch to system theme',
|
|
14
|
+
system: 'Switch to light theme',
|
|
15
|
+
};
|
|
16
|
+
function ThemeToggleMenuItem() {
|
|
17
|
+
const { theme, setTheme } = useTheme();
|
|
18
|
+
const Icon = THEME_ICON[theme];
|
|
19
|
+
return (_jsxs(DropdownMenuItem, { onClick: () => setTheme(NEXT_THEME[theme]), children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), THEME_LABEL[theme]] }));
|
|
20
|
+
}
|
|
21
|
+
export function WorkspaceSwitcher({ workspaceMenuFooter } = {}) {
|
|
22
|
+
const { workspace, workspaces } = useMedaShell();
|
|
23
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", className: "flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-accent" }), children: [workspace.icon != null && (_jsx("span", { className: "shrink-0", "aria-hidden": "true", children: workspace.icon })), _jsx("span", { children: workspace.name }), _jsx(ChevronDown, { size: 14, "aria-hidden": "true" })] }), _jsxs(DropdownMenuContent, { className: "min-w-[200px]", children: [workspaces.length > 0 && (_jsxs(_Fragment, { children: [workspaces.map((ws) => (_jsxs(DropdownMenuItem, { children: [ws.icon != null && _jsx("span", { "aria-hidden": "true", children: ws.icon }), ws.name] }, ws.id))), _jsx(DropdownMenuSeparator, {})] })), _jsx(DropdownMenuItem, { children: "Manage workspaces" }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { children: "Settings" }), _jsx(DropdownMenuItem, { children: "Profile" }), _jsx(DropdownMenuSeparator, {}), _jsx(ThemeToggleMenuItem, {}), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { children: "Sign out" }), workspaceMenuFooter] })] }));
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// AppTabs
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
export function AppTabs() {
|
|
29
|
+
const { apps, activeAppId, setActiveApp } = useMedaShell();
|
|
30
|
+
return (_jsx("nav", { "aria-label": "Applications", className: "flex items-center", children: apps.map((app) => {
|
|
31
|
+
const isActive = app.id === activeAppId;
|
|
32
|
+
const Icon = app.icon;
|
|
33
|
+
return (_jsxs("button", { type: "button", "aria-current": isActive ? 'page' : undefined, onClick: () => {
|
|
34
|
+
setActiveApp(app.id);
|
|
35
|
+
// TODO(Phase 18.x): renderLink integration so clicking a tab also navigates
|
|
36
|
+
// the consumer's router; setActiveApp alone updates context.
|
|
37
|
+
}, onMouseEnter: () => { }, className: cn('flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors', isActive
|
|
38
|
+
? 'border-b-2 border-primary text-foreground'
|
|
39
|
+
: 'text-muted-foreground hover:text-foreground'), children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), app.label] }, app.id));
|
|
40
|
+
}) }));
|
|
41
|
+
}
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// PanelToggle
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
export function PanelToggle() {
|
|
46
|
+
const { panel } = useMedaShell();
|
|
47
|
+
const isOpen = panel.mode !== 'closed';
|
|
48
|
+
const handleClick = () => {
|
|
49
|
+
panel.setMode(isOpen ? 'closed' : 'panel');
|
|
50
|
+
};
|
|
51
|
+
return (_jsx("button", { type: "button", "aria-label": isOpen ? 'Close right panel' : 'Open right panel', onClick: handleClick, className: cn('inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors', isOpen ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent'), children: isOpen ? (_jsx(PanelRightClose, { size: 18, "aria-hidden": "true" })) : (_jsx(PanelRightOpen, { size: 18, "aria-hidden": "true" })) }));
|
|
52
|
+
}
|
|
53
|
+
export function ShellHeader({ globalActions, className } = {}) {
|
|
54
|
+
const band = useShellViewport();
|
|
55
|
+
if (band === 'mobile')
|
|
56
|
+
return null;
|
|
57
|
+
return (_jsxs("header", { className: cn('flex h-[var(--shell-header-height)] w-full items-center justify-between', 'border-b border-border bg-background px-3', className), children: [_jsxs("div", { className: "flex items-center", children: [_jsx(WorkspaceSwitcher, {}), _jsx(AppTabs, {})] }), _jsxs("div", { className: "flex items-center gap-2", children: [globalActions, _jsx(PanelToggle, {})] })] }));
|
|
15
58
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ShellContentLayout, ShellViewportBand } from './types.js';
|
|
2
|
-
export declare function getShellContentMaxWidth(layout: ShellContentLayout, viewportBand: ShellViewportBand):
|
|
1
|
+
import type { ShellContentLayout, ShellViewportBand } from './extras/types.js';
|
|
2
|
+
export declare function getShellContentMaxWidth(layout: ShellContentLayout, viewportBand: ShellViewportBand): 1440 | 1760 | 1920 | 1120 | 1280 | 1400 | undefined;
|
|
3
3
|
export declare function getResolvedShellPanelWidth({ preferredWidth, viewportWidth, sidebarOpen, sidebarWidth, }: {
|
|
4
4
|
preferredWidth: number;
|
|
5
5
|
viewportWidth: number;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ShellMainLayout } from './types.js';
|
|
3
|
+
export interface ShellMainProps {
|
|
4
|
+
layout?: ShellMainLayout;
|
|
5
|
+
className?: string;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare function ShellMain({ layout, className, children }: ShellMainProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
const layoutClass = {
|
|
5
|
+
workspace: 'mx-auto w-full max-w-[1280px] px-4 py-6 sm:px-6 lg:px-8',
|
|
6
|
+
centered: 'mx-auto w-full max-w-2xl px-4 py-6 sm:px-6',
|
|
7
|
+
fullbleed: 'w-full',
|
|
8
|
+
};
|
|
9
|
+
export function ShellMain({ layout = 'workspace', className, children }) {
|
|
10
|
+
return (_jsx("main", { "data-meda-shell-main-layout": layout, className: cn('flex-1 min-w-0 overflow-y-auto bg-shell-main', '[content-visibility:auto]', layoutClass[layout], className), children: children }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ShellStorageAdapter } from './layout-state.js';
|
|
3
|
+
import type { AppDefinition, MobileBottomNavItem, PanelMode, ThemeAdapter, WorkspaceDefinition } from './types.js';
|
|
4
|
+
export type MobileDrawerKind = 'menu-drawer' | 'module-drawer' | 'panels-drawer' | 'ai-drawer' | (string & {}) | null;
|
|
5
|
+
interface MedaShellContextValue {
|
|
6
|
+
workspace: WorkspaceDefinition;
|
|
7
|
+
workspaces: WorkspaceDefinition[];
|
|
8
|
+
apps: AppDefinition[];
|
|
9
|
+
activeAppId: string;
|
|
10
|
+
setActiveApp: (id: string) => void;
|
|
11
|
+
panel: {
|
|
12
|
+
mode: PanelMode;
|
|
13
|
+
activeView: string | null;
|
|
14
|
+
width: number;
|
|
15
|
+
setMode: (m: PanelMode) => void;
|
|
16
|
+
setActiveView: (v: string | null) => void;
|
|
17
|
+
setWidth: (w: number) => void;
|
|
18
|
+
/** Opens panel + switches to viewId in one call.
|
|
19
|
+
* Sugar for app keyboard shortcuts (e.g. Cmd+J → panel.focus('ai')).
|
|
20
|
+
* If already open in 'panel', 'expanded', or 'fullscreen', the existing
|
|
21
|
+
* mode is preserved — only flips closed → panel. */
|
|
22
|
+
focus: (viewId: string) => void;
|
|
23
|
+
};
|
|
24
|
+
contextRail: {
|
|
25
|
+
width: number;
|
|
26
|
+
collapsed: boolean;
|
|
27
|
+
setWidth: (w: number) => void;
|
|
28
|
+
setCollapsed: (c: boolean) => void;
|
|
29
|
+
};
|
|
30
|
+
mobileBottomNav: MobileBottomNavItem[];
|
|
31
|
+
mobileDrawer: {
|
|
32
|
+
open: MobileDrawerKind;
|
|
33
|
+
setOpen: (kind: MobileDrawerKind) => void;
|
|
34
|
+
};
|
|
35
|
+
commandPalette: {
|
|
36
|
+
open: boolean;
|
|
37
|
+
setOpen: (open: boolean) => void;
|
|
38
|
+
};
|
|
39
|
+
commandPaletteHotkey: string;
|
|
40
|
+
/** Selection bridge between main workspace and right panel views (spec §17). */
|
|
41
|
+
selection: unknown | null;
|
|
42
|
+
setSelection: (value: unknown | null) => void;
|
|
43
|
+
}
|
|
44
|
+
export declare function useMedaShell(): MedaShellContextValue;
|
|
45
|
+
/**
|
|
46
|
+
* Selection bridge between main workspace and right panel views (spec §17).
|
|
47
|
+
*
|
|
48
|
+
* `T` is consumer-typed: setSelection writes whatever shape you pass; the
|
|
49
|
+
* matching getter narrows to `T | null`. The shell does not validate the
|
|
50
|
+
* shape — coordinate types between writer and readers in your app.
|
|
51
|
+
*/
|
|
52
|
+
export declare function useShellSelection<T>(): readonly [T | null, (value: T | null) => void];
|
|
53
|
+
export interface MedaShellProviderProps {
|
|
54
|
+
workspace: WorkspaceDefinition;
|
|
55
|
+
workspaces?: WorkspaceDefinition[];
|
|
56
|
+
apps: AppDefinition[];
|
|
57
|
+
defaultActiveApp?: string;
|
|
58
|
+
storage?: ShellStorageAdapter;
|
|
59
|
+
mobileBottomNav?: MobileBottomNavItem[];
|
|
60
|
+
commandPaletteHotkey?: string;
|
|
61
|
+
themeAdapter?: 'default' | 'next-themes' | ThemeAdapter;
|
|
62
|
+
children: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
export declare function MedaShellProvider(props: MedaShellProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { LayoutGrid, Menu, PanelTop, Sparkles } from 'lucide-react';
|
|
4
|
+
import { createContext, lazy, Suspense, useContext, useMemo, useState, } from 'react';
|
|
5
|
+
import { createLocalStorageAdapter, useShellLayoutState, } from './layout-state.js';
|
|
6
|
+
import { DefaultThemeProvider, ThemeCtx } from './theme.js';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Theme adapter wiring
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Lazy-loaded so that importing next-themes only happens when the consumer
|
|
11
|
+
// passes themeAdapter='next-themes'. Default-adapter consumers never trigger
|
|
12
|
+
// the import.
|
|
13
|
+
const NextThemesAdapter = lazy(() => import('./theme-next-themes.js').then((m) => ({ default: m.NextThemesAdapter })));
|
|
14
|
+
function CustomThemeBridge({ adapter, children }) {
|
|
15
|
+
return _jsx(ThemeCtx.Provider, { value: adapter, children: children });
|
|
16
|
+
}
|
|
17
|
+
function pickTheme(themeAdapter, children) {
|
|
18
|
+
const adapter = themeAdapter ?? 'default';
|
|
19
|
+
if (adapter === 'default') {
|
|
20
|
+
return _jsx(DefaultThemeProvider, { children: children });
|
|
21
|
+
}
|
|
22
|
+
if (adapter === 'next-themes') {
|
|
23
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(NextThemesAdapter, { children: children }) }));
|
|
24
|
+
}
|
|
25
|
+
// Custom ThemeAdapter object
|
|
26
|
+
return _jsx(CustomThemeBridge, { adapter: adapter, children: children });
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Default mobile bottom nav (canonical 4-item set, per spec §16)
|
|
30
|
+
// Full icons ship here; the per-app customisation point is the mobileBottomNav
|
|
31
|
+
// prop. For RC.1 the canonical drawer wire-up is a Phase 13 concern — only
|
|
32
|
+
// the shape is locked here.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const defaultMobileBottomNav = [
|
|
35
|
+
{ id: 'menu', label: 'Menu', icon: Menu, opens: 'menu-drawer' },
|
|
36
|
+
{ id: 'module', label: 'Module', icon: LayoutGrid, opens: 'module-drawer' },
|
|
37
|
+
{ id: 'panels', label: 'Panels', icon: PanelTop, opens: 'panels-drawer' },
|
|
38
|
+
{ id: 'ai', label: 'AI', icon: Sparkles, opens: 'ai-drawer' },
|
|
39
|
+
];
|
|
40
|
+
const Ctx = createContext(null);
|
|
41
|
+
export function useMedaShell() {
|
|
42
|
+
const v = useContext(Ctx);
|
|
43
|
+
if (!v)
|
|
44
|
+
throw new Error('useMedaShell must be used inside <MedaShellProvider>');
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Selection bridge between main workspace and right panel views (spec §17).
|
|
49
|
+
*
|
|
50
|
+
* `T` is consumer-typed: setSelection writes whatever shape you pass; the
|
|
51
|
+
* matching getter narrows to `T | null`. The shell does not validate the
|
|
52
|
+
* shape — coordinate types between writer and readers in your app.
|
|
53
|
+
*/
|
|
54
|
+
export function useShellSelection() {
|
|
55
|
+
const ctx = useMedaShell();
|
|
56
|
+
return [ctx.selection, ctx.setSelection];
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Provider implementation
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
export function MedaShellProvider(props) {
|
|
62
|
+
// Guard: apps array must not be empty
|
|
63
|
+
if (props.apps.length === 0) {
|
|
64
|
+
throw new Error('MedaShellProvider: apps must contain at least one AppDefinition');
|
|
65
|
+
}
|
|
66
|
+
// Stable storage reference — prevents useShellLayoutState's hydration
|
|
67
|
+
// effect from looping when an inline adapter would change identity each render.
|
|
68
|
+
const storage = useMemo(() => props.storage ?? createLocalStorageAdapter(), [props.storage]);
|
|
69
|
+
const [activeAppId, setActiveApp] = useState(props.defaultActiveApp ?? props.apps[0]?.id ?? '');
|
|
70
|
+
const [selection, setSelection] = useState(null);
|
|
71
|
+
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(null);
|
|
72
|
+
const mobileDrawer = useMemo(() => ({
|
|
73
|
+
open: mobileDrawerOpen,
|
|
74
|
+
setOpen: setMobileDrawerOpen,
|
|
75
|
+
}), [mobileDrawerOpen]);
|
|
76
|
+
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
77
|
+
const commandPalette = useMemo(() => ({
|
|
78
|
+
open: commandPaletteOpen,
|
|
79
|
+
setOpen: setCommandPaletteOpen,
|
|
80
|
+
}), [commandPaletteOpen]);
|
|
81
|
+
const [layoutState, setLayoutState] = useShellLayoutState({
|
|
82
|
+
workspaceId: props.workspace.id,
|
|
83
|
+
appId: activeAppId,
|
|
84
|
+
storage,
|
|
85
|
+
});
|
|
86
|
+
const panel = useMemo(() => ({
|
|
87
|
+
mode: layoutState.rightPanel.mode,
|
|
88
|
+
activeView: layoutState.rightPanel.activeView,
|
|
89
|
+
width: layoutState.rightPanel.width,
|
|
90
|
+
setMode: (mode) => setLayoutState((prev) => ({
|
|
91
|
+
...prev,
|
|
92
|
+
rightPanel: { ...prev.rightPanel, mode },
|
|
93
|
+
})),
|
|
94
|
+
setActiveView: (activeView) => setLayoutState((prev) => ({
|
|
95
|
+
...prev,
|
|
96
|
+
rightPanel: { ...prev.rightPanel, activeView },
|
|
97
|
+
})),
|
|
98
|
+
setWidth: (width) => setLayoutState((prev) => ({
|
|
99
|
+
...prev,
|
|
100
|
+
rightPanel: { ...prev.rightPanel, width },
|
|
101
|
+
})),
|
|
102
|
+
// focus(viewId) — opens panel + switches to view in one call.
|
|
103
|
+
// Only flips closed → panel; preserves expanded / fullscreen modes.
|
|
104
|
+
focus: (viewId) => setLayoutState((prev) => {
|
|
105
|
+
const nextMode = prev.rightPanel.mode === 'closed' ? 'panel' : prev.rightPanel.mode;
|
|
106
|
+
return {
|
|
107
|
+
...prev,
|
|
108
|
+
rightPanel: { ...prev.rightPanel, mode: nextMode, activeView: viewId },
|
|
109
|
+
};
|
|
110
|
+
}),
|
|
111
|
+
}), [layoutState, setLayoutState]);
|
|
112
|
+
const contextRail = useMemo(() => ({
|
|
113
|
+
width: layoutState.contextRail.width,
|
|
114
|
+
collapsed: layoutState.contextRail.collapsed,
|
|
115
|
+
setWidth: (width) => setLayoutState((prev) => ({
|
|
116
|
+
...prev,
|
|
117
|
+
contextRail: { ...prev.contextRail, width },
|
|
118
|
+
})),
|
|
119
|
+
setCollapsed: (collapsed) => setLayoutState((prev) => ({
|
|
120
|
+
...prev,
|
|
121
|
+
contextRail: { ...prev.contextRail, collapsed },
|
|
122
|
+
})),
|
|
123
|
+
}), [layoutState, setLayoutState]);
|
|
124
|
+
const value = useMemo(() => ({
|
|
125
|
+
workspace: props.workspace,
|
|
126
|
+
workspaces: props.workspaces ?? [props.workspace],
|
|
127
|
+
apps: props.apps,
|
|
128
|
+
activeAppId,
|
|
129
|
+
setActiveApp,
|
|
130
|
+
panel,
|
|
131
|
+
contextRail,
|
|
132
|
+
mobileBottomNav: props.mobileBottomNav ?? defaultMobileBottomNav,
|
|
133
|
+
mobileDrawer,
|
|
134
|
+
commandPalette,
|
|
135
|
+
commandPaletteHotkey: props.commandPaletteHotkey ?? 'mod+k',
|
|
136
|
+
selection,
|
|
137
|
+
setSelection,
|
|
138
|
+
}), [
|
|
139
|
+
props.workspace,
|
|
140
|
+
props.workspaces,
|
|
141
|
+
props.apps,
|
|
142
|
+
activeAppId,
|
|
143
|
+
panel,
|
|
144
|
+
contextRail,
|
|
145
|
+
props.mobileBottomNav,
|
|
146
|
+
mobileDrawer,
|
|
147
|
+
commandPalette,
|
|
148
|
+
props.commandPaletteHotkey,
|
|
149
|
+
selection,
|
|
150
|
+
]);
|
|
151
|
+
return _jsx(Ctx.Provider, { value: value, children: pickTheme(props.themeAdapter, props.children) });
|
|
152
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { ShellContentLayout, ShellTab } from './types.js';
|
|
2
|
+
import type { ShellContentLayout, ShellTab } from './extras/types.js';
|
|
3
3
|
interface ShellRouteMatchLike {
|
|
4
4
|
handle?: unknown;
|
|
5
5
|
params?: Record<string, string | undefined>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { ThemeProvider as NextThemesProvider, useTheme as useNextTheme } from 'next-themes';
|
|
4
|
+
import { ThemeCtx } from './theme.js';
|
|
5
|
+
function narrow(value) {
|
|
6
|
+
if (value === 'light' || value === 'dark' || value === 'system')
|
|
7
|
+
return value;
|
|
8
|
+
return 'system';
|
|
9
|
+
}
|
|
10
|
+
function narrowResolved(value) {
|
|
11
|
+
return value === 'dark' ? 'dark' : 'light';
|
|
12
|
+
}
|
|
13
|
+
function NextThemesBridge({ children }) {
|
|
14
|
+
const { theme, setTheme, resolvedTheme } = useNextTheme();
|
|
15
|
+
const value = {
|
|
16
|
+
theme: narrow(theme),
|
|
17
|
+
setTheme: (t) => setTheme(t),
|
|
18
|
+
resolvedTheme: narrowResolved(resolvedTheme),
|
|
19
|
+
};
|
|
20
|
+
return _jsx(ThemeCtx.Provider, { value: value, children: children });
|
|
21
|
+
}
|
|
22
|
+
export function NextThemesAdapter({ children }) {
|
|
23
|
+
return (_jsx(NextThemesProvider, { attribute: "class", enableSystem: true, defaultTheme: "system", children: _jsx(NextThemesBridge, { children: children }) }));
|
|
24
|
+
}
|