@medalsocial/meda 0.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 +268 -0
- package/components.json +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/shell/index.d.ts +15 -0
- package/dist/shell/index.js +15 -0
- package/dist/shell/navigation-area.d.ts +4 -0
- package/dist/shell/navigation-area.js +4 -0
- package/dist/shell/public.d.ts +4 -0
- package/dist/shell/public.js +3 -0
- package/dist/shell/shell-app-rail.d.ts +18 -0
- package/dist/shell/shell-app-rail.js +27 -0
- package/dist/shell/shell-desktop-panel-dock.d.ts +14 -0
- package/dist/shell/shell-desktop-panel-dock.js +43 -0
- package/dist/shell/shell-frame.d.ts +13 -0
- package/dist/shell/shell-frame.js +4 -0
- package/dist/shell/shell-header.d.ts +11 -0
- package/dist/shell/shell-header.js +15 -0
- package/dist/shell/shell-layout-utils.d.ts +8 -0
- package/dist/shell/shell-layout-utils.js +38 -0
- package/dist/shell/shell-module-nav.d.ts +29 -0
- package/dist/shell/shell-module-nav.js +16 -0
- package/dist/shell/shell-panel-rail.d.ts +9 -0
- package/dist/shell/shell-panel-rail.js +17 -0
- package/dist/shell/shell-route-utils.d.ts +11 -0
- package/dist/shell/shell-route-utils.js +32 -0
- package/dist/shell/shell-scrollable-content.d.ts +9 -0
- package/dist/shell/shell-scrollable-content.js +7 -0
- package/dist/shell/shell-state.d.ts +39 -0
- package/dist/shell/shell-state.js +132 -0
- package/dist/shell/shell-tab-bar.d.ts +18 -0
- package/dist/shell/shell-tab-bar.js +27 -0
- package/dist/shell/types.d.ts +64 -0
- package/dist/shell/types.js +1 -0
- package/dist/shell/utils.d.ts +43 -0
- package/dist/shell/utils.js +50 -0
- package/dist/shell/workbench-layout.d.ts +10 -0
- package/dist/shell/workbench-layout.js +29 -0
- package/dist/styles.css +4 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# @medalsocial/meda
|
|
2
|
+
|
|
3
|
+
Shared Meda UI shell/runtime package.
|
|
4
|
+
|
|
5
|
+
Current scope:
|
|
6
|
+
- shell layout primitives
|
|
7
|
+
- shell navigation and rail renderers
|
|
8
|
+
- shell header and tab chrome
|
|
9
|
+
- shell state runtime
|
|
10
|
+
- shell layout and route utilities
|
|
11
|
+
|
|
12
|
+
Non-goals for this package:
|
|
13
|
+
- duplicating the full shadcn primitive layer
|
|
14
|
+
- Picasso-specific route config, workspace data, or panel content
|
|
15
|
+
- router-specific runtime assumptions
|
|
16
|
+
|
|
17
|
+
## Import Surface
|
|
18
|
+
|
|
19
|
+
Primary root import:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
NavigationArea,
|
|
24
|
+
ShellAppRail,
|
|
25
|
+
ShellDesktopPanelDock,
|
|
26
|
+
ShellFrame,
|
|
27
|
+
ShellHeaderFrame,
|
|
28
|
+
ShellModuleNav,
|
|
29
|
+
ShellPanelRail,
|
|
30
|
+
ShellScrollableContent,
|
|
31
|
+
ShellStateProvider,
|
|
32
|
+
ShellTabBar,
|
|
33
|
+
WorkbenchLayout,
|
|
34
|
+
} from '@medalsocial/meda'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The root package is the curated public API. It is the default import surface for app consumers.
|
|
38
|
+
|
|
39
|
+
Shell subpath:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { ShellFrame, ShellStateProvider } from '@medalsocial/meda/shell'
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The `@medalsocial/meda/shell` subpath is broader. Use it when you intentionally need lower-level shell helpers that are not part of the primary root contract yet.
|
|
46
|
+
|
|
47
|
+
## Stable Root Surface
|
|
48
|
+
|
|
49
|
+
Root exports currently include:
|
|
50
|
+
- shell chrome: `ShellFrame`, `ShellHeaderFrame`, `ShellPanelToggle`, `NavigationArea`
|
|
51
|
+
- shell navigation: `ShellAppRail`, `ShellModuleNav`, `ShellPanelRail`, `ShellTabBar`
|
|
52
|
+
- shell layout/runtime: `ShellDesktopPanelDock`, `ShellScrollableContent`, `WorkbenchLayout`
|
|
53
|
+
- shell state: `ShellStateProvider`, `useShellState`
|
|
54
|
+
- shared contracts: `ShellModuleDefinition`, `ShellPanelDefinition`, `ShellRailItem`, `ShellTab`, `ShellViewDefinition`
|
|
55
|
+
- shared utilities: route-handle helpers, panel helpers, layout helpers, section-command builder, shortcut-map builder
|
|
56
|
+
|
|
57
|
+
Lower-level shell internals remain available from `@medalsocial/meda/shell`.
|
|
58
|
+
|
|
59
|
+
## Minimal Consumer Shape
|
|
60
|
+
|
|
61
|
+
Typical consumer setup:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
ShellFrame,
|
|
66
|
+
ShellHeaderFrame,
|
|
67
|
+
ShellStateProvider,
|
|
68
|
+
ShellTabBar,
|
|
69
|
+
type ShellModuleDefinition,
|
|
70
|
+
} from '@medalsocial/meda'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then the host app provides:
|
|
74
|
+
- router integration through `renderLink` callbacks and search-param adapters
|
|
75
|
+
- module definitions and panel registries
|
|
76
|
+
- workspace/business state
|
|
77
|
+
- app-specific menus and content renderers
|
|
78
|
+
|
|
79
|
+
## Recipes
|
|
80
|
+
|
|
81
|
+
### 1. Define app modules
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { Home, Package } from 'lucide-react'
|
|
85
|
+
import type { ShellModuleDefinition } from '@medalsocial/meda'
|
|
86
|
+
|
|
87
|
+
export const modules: Record<string, ShellModuleDefinition> = {
|
|
88
|
+
lab: {
|
|
89
|
+
id: 'lab',
|
|
90
|
+
label: 'Labs',
|
|
91
|
+
description: 'Hardware R&D workspace',
|
|
92
|
+
items: [
|
|
93
|
+
{
|
|
94
|
+
to: '/lab',
|
|
95
|
+
label: 'Overview',
|
|
96
|
+
description: 'Program status and entry points',
|
|
97
|
+
icon: Home,
|
|
98
|
+
shortcut: '1',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
to: '/lab/products',
|
|
102
|
+
label: 'Products',
|
|
103
|
+
description: 'Every form factor and BOM',
|
|
104
|
+
icon: Package,
|
|
105
|
+
shortcut: '2',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
headerTabs: [
|
|
109
|
+
{ id: 'overview', to: '/lab', label: 'Overview' },
|
|
110
|
+
{ id: 'products', to: '/lab/products', label: 'Products' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2. Connect shell state to host routing
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { ShellStateProvider } from '@medalsocial/meda'
|
|
120
|
+
import { useSearchParams } from 'react-router'
|
|
121
|
+
|
|
122
|
+
export function AppShellState({ children }: { children: React.ReactNode }) {
|
|
123
|
+
const [searchParams, setSearchParams] = useSearchParams()
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<ShellStateProvider
|
|
127
|
+
adapter={{
|
|
128
|
+
searchParams,
|
|
129
|
+
setSearchParams: (updater) => {
|
|
130
|
+
setSearchParams((current) => updater(current))
|
|
131
|
+
},
|
|
132
|
+
}}
|
|
133
|
+
initialActiveRail="lab"
|
|
134
|
+
selectionQueryParam="product"
|
|
135
|
+
sidePanelWidthStorageKey="my-app.side-panel.width"
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</ShellStateProvider>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3. Render router-aware tabs
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { ShellTabBar, type ShellTab } from '@medalsocial/meda'
|
|
147
|
+
import Link from 'next/link'
|
|
148
|
+
|
|
149
|
+
export function AppTabs({ tabs, activeTab }: { tabs: ShellTab[]; activeTab?: string }) {
|
|
150
|
+
return (
|
|
151
|
+
<ShellTabBar
|
|
152
|
+
tabs={tabs}
|
|
153
|
+
activeTab={activeTab}
|
|
154
|
+
ariaLabel="Workspace tabs"
|
|
155
|
+
renderLink={({ children, className, tab, isActive }) => (
|
|
156
|
+
<Link
|
|
157
|
+
key={tab.id}
|
|
158
|
+
href={tab.to ?? '#'}
|
|
159
|
+
aria-current={isActive ? 'page' : undefined}
|
|
160
|
+
className={className}
|
|
161
|
+
>
|
|
162
|
+
{children}
|
|
163
|
+
</Link>
|
|
164
|
+
)}
|
|
165
|
+
/>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 4. Compose the shell frame
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<ShellFrame
|
|
174
|
+
header={
|
|
175
|
+
<ShellHeaderFrame
|
|
176
|
+
left={<WorkspaceMenu />}
|
|
177
|
+
center={<AppTabs tabs={tabs} activeTab={activeTab} />}
|
|
178
|
+
right={<HeaderActions />}
|
|
179
|
+
/>
|
|
180
|
+
}
|
|
181
|
+
navigation={
|
|
182
|
+
<NavigationArea>
|
|
183
|
+
<AppRail />
|
|
184
|
+
<SectionSidebar />
|
|
185
|
+
</NavigationArea>
|
|
186
|
+
}
|
|
187
|
+
content={
|
|
188
|
+
<ShellScrollableContent
|
|
189
|
+
layout="workspace"
|
|
190
|
+
maxWidth={1120}
|
|
191
|
+
desktopDockOffset={panelOpen ? 392 : 0}
|
|
192
|
+
>
|
|
193
|
+
<Outlet />
|
|
194
|
+
</ShellScrollableContent>
|
|
195
|
+
}
|
|
196
|
+
desktopDock={
|
|
197
|
+
panelOpen ? (
|
|
198
|
+
<ShellDesktopPanelDock
|
|
199
|
+
defaultView={activeView}
|
|
200
|
+
panelOpen
|
|
201
|
+
width={panelWidth}
|
|
202
|
+
onWidthChange={setPanelWidth}
|
|
203
|
+
renderPanel={({ defaultView, className }) => (
|
|
204
|
+
<MyPanel defaultView={defaultView} className={className} />
|
|
205
|
+
)}
|
|
206
|
+
/>
|
|
207
|
+
) : null
|
|
208
|
+
}
|
|
209
|
+
/>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 5. Build section commands
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { buildShellSectionCommands } from '@medalsocial/meda'
|
|
216
|
+
|
|
217
|
+
const commands = [
|
|
218
|
+
...buildShellSectionCommands(activeModule),
|
|
219
|
+
{ id: 'settings', label: 'Settings', to: '/settings', icon: Settings, group: 'Global' },
|
|
220
|
+
]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 6. Resolve route-provided shell metadata
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import {
|
|
227
|
+
getShellActionsFromMatches,
|
|
228
|
+
getShellContentLayoutFromMatches,
|
|
229
|
+
getShellPanelViewsFromMatches,
|
|
230
|
+
getShellTabsFromMatches,
|
|
231
|
+
} from '@medalsocial/meda'
|
|
232
|
+
|
|
233
|
+
const contentLayout = getShellContentLayoutFromMatches(matches)
|
|
234
|
+
const shellTabs = getShellTabsFromMatches(matches, pathname)
|
|
235
|
+
const shellActions = getShellActionsFromMatches(matches, pathname)
|
|
236
|
+
const panelViewIds = getShellPanelViewsFromMatches(matches)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Host Responsibilities
|
|
240
|
+
|
|
241
|
+
Consumers own:
|
|
242
|
+
- routing and link rendering
|
|
243
|
+
- module definitions
|
|
244
|
+
- panel definitions and renderers
|
|
245
|
+
- workspace/business state
|
|
246
|
+
- app-specific header/workspace menus
|
|
247
|
+
|
|
248
|
+
Meda owns:
|
|
249
|
+
- reusable shell chrome and runtime
|
|
250
|
+
- generic shell state persistence and query-param wiring through adapters
|
|
251
|
+
- layout math and route-handle helpers
|
|
252
|
+
|
|
253
|
+
## Router Guidance
|
|
254
|
+
|
|
255
|
+
Meda should stay React-first and router-agnostic.
|
|
256
|
+
|
|
257
|
+
- React Router apps: pass `Link`/`NavLink` through render props and connect `useSearchParams` to `ShellStateProvider`
|
|
258
|
+
- Next.js apps: pass `next/link` wrappers and a host search-param adapter
|
|
259
|
+
- TanStack Router apps: pass router link wrappers and route/search adapters from the host
|
|
260
|
+
|
|
261
|
+
The package should not import app-router internals directly.
|
|
262
|
+
|
|
263
|
+
## Current Verification
|
|
264
|
+
|
|
265
|
+
The package is currently verified by:
|
|
266
|
+
- package-level unit tests in `software/meda/src/shell/*.test.tsx`
|
|
267
|
+
- Picasso dashboard consuming the package in `software/dashboard`
|
|
268
|
+
- a separate consumer fixture in `software/meda-fixture`
|
package/components.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "default",
|
|
4
|
+
"iconLibrary": "lucide",
|
|
5
|
+
"rsc": false,
|
|
6
|
+
"tsx": true,
|
|
7
|
+
"tailwind": {
|
|
8
|
+
"config": "",
|
|
9
|
+
"css": "src/styles/globals.css",
|
|
10
|
+
"baseColor": "neutral",
|
|
11
|
+
"cssVariables": true
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/shell",
|
|
15
|
+
"ui": "@/ui",
|
|
16
|
+
"lib": "@/lib",
|
|
17
|
+
"hooks": "@/hooks",
|
|
18
|
+
"utils": "@/lib/utils"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './shell/public';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './shell/public';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './navigation-area';
|
|
2
|
+
export * from './shell-app-rail';
|
|
3
|
+
export * from './shell-desktop-panel-dock';
|
|
4
|
+
export * from './shell-frame';
|
|
5
|
+
export * from './shell-header';
|
|
6
|
+
export * from './shell-layout-utils';
|
|
7
|
+
export * from './shell-module-nav';
|
|
8
|
+
export * from './shell-panel-rail';
|
|
9
|
+
export * from './shell-route-utils';
|
|
10
|
+
export * from './shell-scrollable-content';
|
|
11
|
+
export * from './shell-state';
|
|
12
|
+
export * from './shell-tab-bar';
|
|
13
|
+
export * from './types';
|
|
14
|
+
export * from './utils';
|
|
15
|
+
export * from './workbench-layout';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './navigation-area';
|
|
2
|
+
export * from './shell-app-rail';
|
|
3
|
+
export * from './shell-desktop-panel-dock';
|
|
4
|
+
export * from './shell-frame';
|
|
5
|
+
export * from './shell-header';
|
|
6
|
+
export * from './shell-layout-utils';
|
|
7
|
+
export * from './shell-module-nav';
|
|
8
|
+
export * from './shell-panel-rail';
|
|
9
|
+
export * from './shell-route-utils';
|
|
10
|
+
export * from './shell-scrollable-content';
|
|
11
|
+
export * from './shell-state';
|
|
12
|
+
export * from './shell-tab-bar';
|
|
13
|
+
export * from './types';
|
|
14
|
+
export * from './utils';
|
|
15
|
+
export * from './workbench-layout';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { NavigationArea, ShellAppRail, ShellDesktopPanelDock, ShellFrame, ShellHeaderFrame, ShellPanelToggle, ShellModuleNav, ShellPanelRail, ShellScrollableContent, ShellStateProvider, ShellTabBar, WorkbenchLayout, } from './index';
|
|
2
|
+
export { buildShellSectionCommands, buildShellShortcutMap, getDefaultShellPanelView, getShellPanelCollection, getShellPanelView, getShellActionsFromMatches, getShellContentLayoutFromMatches, getShellContentMaxWidth, getShellPanelViewsFromMatches, getShellTabsFromMatches, getResolvedShellPanelWidth, isShellPanelViewAvailable, resolveShellPanelView, } from './index';
|
|
3
|
+
export type { ShellCommandDefinition, ShellContentLayout, ShellHostAdapter, ShellModuleDefinition, ShellNavItem, ShellPanelDefinition, ShellRailItem, ShellRenderContext, ShellRouteContext, ShellSearchParamsAdapter, ShellStateContextValue, ShellTab, ShellViewDefinition, ShellViewportBand, } from './index';
|
|
4
|
+
export { useShellState } from './index';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { NavigationArea, ShellAppRail, ShellDesktopPanelDock, ShellFrame, ShellHeaderFrame, ShellPanelToggle, ShellModuleNav, ShellPanelRail, ShellScrollableContent, ShellStateProvider, ShellTabBar, WorkbenchLayout, } from './index';
|
|
2
|
+
export { buildShellSectionCommands, buildShellShortcutMap, getDefaultShellPanelView, getShellPanelCollection, getShellPanelView, getShellActionsFromMatches, getShellContentLayoutFromMatches, getShellContentMaxWidth, getShellPanelViewsFromMatches, getShellTabsFromMatches, getResolvedShellPanelWidth, isShellPanelViewAvailable, resolveShellPanelView, } from './index';
|
|
3
|
+
export { useShellState } from './index';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ShellRailItem } from './types';
|
|
3
|
+
interface ShellAppRailRenderArgs {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className: string;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
item: ShellRailItem;
|
|
8
|
+
}
|
|
9
|
+
export interface ShellAppRailProps {
|
|
10
|
+
mainItems: ShellRailItem[];
|
|
11
|
+
utilityItems: ShellRailItem[];
|
|
12
|
+
className?: string;
|
|
13
|
+
footer?: ReactNode;
|
|
14
|
+
isItemActive: (item: ShellRailItem) => boolean;
|
|
15
|
+
renderLink: (args: ShellAppRailRenderArgs) => ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function ShellAppRail({ mainItems, utilityItems, className, footer, isItemActive, renderLink, }: ShellAppRailProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
function joinClasses(...values) {
|
|
5
|
+
return values.filter(Boolean).join(' ');
|
|
6
|
+
}
|
|
7
|
+
function RailDivider({ pinned, onToggle }) {
|
|
8
|
+
return (_jsx("div", { className: "group flex w-full items-center justify-center py-4", children: _jsxs("button", { type: "button", "aria-label": pinned ? 'Push items down' : 'Push items up', onClick: onToggle, className: "flex cursor-pointer flex-col items-center gap-0.5", children: [_jsx("span", { className: "hidden h-3.5 w-6 items-center justify-center text-[var(--app-rail-foreground)] transition-colors group-hover:flex", children: pinned ? _jsx(ChevronDown, { size: 14 }) : _jsx(ChevronUp, { size: 14 }) }), _jsx("span", { className: "block h-0.5 w-6 rounded-full bg-[var(--app-rail-foreground)]/30 transition-colors group-hover:bg-[var(--primary)]" })] }) }));
|
|
9
|
+
}
|
|
10
|
+
function RailSection({ items, isItemActive, renderLink, ariaLabel, className, }) {
|
|
11
|
+
return (_jsx("nav", { "aria-label": ariaLabel, className: className, children: items.map((item) => {
|
|
12
|
+
const isActive = isItemActive(item);
|
|
13
|
+
const Icon = item.icon;
|
|
14
|
+
return renderLink({
|
|
15
|
+
item,
|
|
16
|
+
isActive,
|
|
17
|
+
className: joinClasses('flex w-full flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-2.5 text-center transition-colors', isActive
|
|
18
|
+
? 'bg-[var(--app-rail-item-active-bg)] text-[var(--foreground)]'
|
|
19
|
+
: 'text-[var(--app-rail-foreground)] hover:bg-[var(--surface-highlight)] hover:text-[var(--foreground)]'),
|
|
20
|
+
children: (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 22 }), _jsx("span", { className: "text-[10px] font-medium leading-none", children: item.label })] })),
|
|
21
|
+
});
|
|
22
|
+
}) }));
|
|
23
|
+
}
|
|
24
|
+
export function ShellAppRail({ mainItems, utilityItems, className, footer, isItemActive, renderLink, }) {
|
|
25
|
+
const [pinned, setPinned] = useState(false);
|
|
26
|
+
return (_jsxs("div", { className: joinClasses('flex w-[var(--app-rail-width)] shrink-0 flex-col items-center bg-[var(--app-rail)] px-2 py-4', className), children: [_jsx(RailSection, { items: mainItems, isItemActive: isItemActive, renderLink: renderLink, ariaLabel: "Main navigation", className: "flex flex-col items-center gap-1.5" }), _jsx("div", { className: joinClasses('transition-[flex-grow] duration-300 ease-in-out', pinned ? 'grow-0' : 'grow') }), _jsx(RailDivider, { pinned: pinned, onToggle: () => setPinned(!pinned) }), _jsx(RailSection, { items: utilityItems, isItemActive: isItemActive, renderLink: renderLink, ariaLabel: "Utility navigation", className: "mb-2 flex flex-col items-center gap-1" }), _jsx("div", { className: joinClasses('transition-[flex-grow] duration-300 ease-in-out', pinned ? 'grow' : 'grow-0') }), footer] }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export declare function ShellDesktopPanelDock({ defaultView, panelOpen, viewIds, width, onWidthChange, renderPanel, className, }: {
|
|
3
|
+
defaultView: string;
|
|
4
|
+
panelOpen: boolean;
|
|
5
|
+
viewIds?: string[];
|
|
6
|
+
width: number;
|
|
7
|
+
onWidthChange: (width: number) => void;
|
|
8
|
+
renderPanel: (options: {
|
|
9
|
+
defaultView: string;
|
|
10
|
+
viewIds?: string[];
|
|
11
|
+
className?: string;
|
|
12
|
+
}) => React.ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
function ShellPanelResizeHandle({ width, onWidthChange, }) {
|
|
4
|
+
const startXRef = React.useRef(0);
|
|
5
|
+
const startWidthRef = React.useRef(width);
|
|
6
|
+
const cleanupRef = React.useRef(null);
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
return () => {
|
|
9
|
+
cleanupRef.current?.();
|
|
10
|
+
};
|
|
11
|
+
}, []);
|
|
12
|
+
function handlePointerDown(event) {
|
|
13
|
+
if (event.button !== 0)
|
|
14
|
+
return;
|
|
15
|
+
startXRef.current = event.clientX;
|
|
16
|
+
startWidthRef.current = width;
|
|
17
|
+
const handlePointerMove = (moveEvent) => {
|
|
18
|
+
onWidthChange(startWidthRef.current + (startXRef.current - moveEvent.clientX));
|
|
19
|
+
};
|
|
20
|
+
const handlePointerEnd = () => {
|
|
21
|
+
window.removeEventListener('pointermove', handlePointerMove);
|
|
22
|
+
window.removeEventListener('pointerup', handlePointerEnd);
|
|
23
|
+
window.removeEventListener('pointercancel', handlePointerEnd);
|
|
24
|
+
cleanupRef.current = null;
|
|
25
|
+
};
|
|
26
|
+
cleanupRef.current?.();
|
|
27
|
+
window.addEventListener('pointermove', handlePointerMove);
|
|
28
|
+
window.addEventListener('pointerup', handlePointerEnd);
|
|
29
|
+
window.addEventListener('pointercancel', handlePointerEnd);
|
|
30
|
+
cleanupRef.current = handlePointerEnd;
|
|
31
|
+
}
|
|
32
|
+
return (_jsx("button", { type: "button", "aria-label": "Resize panel", onPointerDown: handlePointerDown, className: "absolute inset-y-0 left-0 z-10 flex w-2 shrink-0 cursor-col-resize items-center justify-center bg-transparent transition-colors hover:bg-[var(--primary)]/10", children: _jsx("span", { className: "h-12 w-px rounded-full bg-[var(--border-subtle)]" }) }));
|
|
33
|
+
}
|
|
34
|
+
export function ShellDesktopPanelDock({ defaultView, panelOpen, viewIds, width, onWidthChange, renderPanel, className, }) {
|
|
35
|
+
return (_jsx("div", { "data-testid": "desktop-panel-dock", "aria-hidden": !panelOpen, "data-state": panelOpen ? 'open' : 'closed', className: panelOpen
|
|
36
|
+
? 'pointer-events-auto absolute inset-y-4 right-4 z-20 translate-x-0 opacity-100 transition-[transform,opacity] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]'
|
|
37
|
+
: 'pointer-events-none absolute inset-y-4 right-4 z-20 translate-x-[calc(100%+1.25rem)] opacity-0 transition-[transform,opacity] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]', style: { width: `${width}px` }, children: _jsxs("div", { className: "relative h-full w-full", children: [_jsx(ShellPanelResizeHandle, { width: width, onWidthChange: onWidthChange }), renderPanel({
|
|
38
|
+
defaultView,
|
|
39
|
+
viewIds,
|
|
40
|
+
className: className ??
|
|
41
|
+
'ml-2 rounded-[var(--meda-shell-panel-dock-radius)] border border-[var(--border-subtle)] bg-[color-mix(in_srgb,var(--side-panel)_90%,transparent)] shadow-[var(--meda-shell-panel-dock-shadow)] backdrop-blur-xl',
|
|
42
|
+
})] }) }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare function ShellFrame({ header, navigation, mobileSidebar, mobileHeader, tabBar, content, desktopDock, tabletPanel, mobileBottomNav, commandPalette, }: {
|
|
3
|
+
header: ReactNode;
|
|
4
|
+
navigation: ReactNode;
|
|
5
|
+
mobileSidebar?: ReactNode;
|
|
6
|
+
mobileHeader?: ReactNode;
|
|
7
|
+
tabBar?: ReactNode;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
desktopDock?: ReactNode;
|
|
10
|
+
tabletPanel?: ReactNode;
|
|
11
|
+
mobileBottomNav?: ReactNode;
|
|
12
|
+
commandPalette?: ReactNode;
|
|
13
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function ShellFrame({ header, navigation, mobileSidebar, mobileHeader, tabBar, content, desktopDock, tabletPanel, mobileBottomNav, commandPalette, }) {
|
|
3
|
+
return (_jsxs("div", { "data-testid": "shell-frame", className: "flex h-screen w-full flex-1 flex-col overflow-hidden bg-[var(--background)]", children: [header, _jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden", children: [navigation, mobileSidebar, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: [mobileHeader, tabBar, _jsxs("div", { className: "relative min-h-0 flex-1 overflow-hidden", children: [content, desktopDock] })] }), tabletPanel, mobileBottomNav] }), commandPalette] }));
|
|
4
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export declare function ShellHeaderFrame({ left, center, right, className, }: {
|
|
3
|
+
left: ReactNode;
|
|
4
|
+
center?: ReactNode;
|
|
5
|
+
right?: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function ShellPanelToggle({ panelOpen, onToggle, }: {
|
|
9
|
+
panelOpen: boolean;
|
|
10
|
+
onToggle: () => void;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PanelRightClose, PanelRightOpen } from 'lucide-react';
|
|
3
|
+
export function ShellHeaderFrame({ left, center, right, className, }) {
|
|
4
|
+
return (_jsxs("header", { role: "banner", className: [
|
|
5
|
+
'relative z-[var(--z-sticky)] grid h-[var(--header-height)] w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 bg-[var(--header)] px-3 shadow-[var(--shadow-header)] sm:px-4',
|
|
6
|
+
className,
|
|
7
|
+
]
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.join(' '), children: [left, _jsx("div", { className: "min-w-0", children: center }), _jsx("div", { "data-testid": "shell-header-actions", className: "hidden items-center justify-end gap-2 md:flex", children: right })] }));
|
|
10
|
+
}
|
|
11
|
+
export function ShellPanelToggle({ panelOpen, onToggle, }) {
|
|
12
|
+
return (_jsx("button", { type: "button", "aria-label": panelOpen ? 'Close panel' : 'Open panel', onClick: onToggle, className: panelOpen
|
|
13
|
+
? 'inline-flex h-9 items-center justify-center rounded-xl border border-[var(--primary)]/30 bg-[var(--primary)]/12 px-2.5 text-[var(--primary)] transition-[background-color,color,border-color] duration-200 hover:bg-[var(--primary)]/18'
|
|
14
|
+
: 'inline-flex h-9 items-center justify-center rounded-xl border border-[var(--border-subtle)] bg-[var(--surface-1)] px-2.5 text-[var(--muted-foreground)] transition-[background-color,color,border-color] duration-200 hover:bg-[var(--sidebar-accent)] hover:text-[var(--foreground)]', children: panelOpen ? _jsx(PanelRightClose, { size: 18 }) : _jsx(PanelRightOpen, { size: 18 }) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ShellContentLayout, ShellViewportBand } from './types';
|
|
2
|
+
export declare function getShellContentMaxWidth(layout: ShellContentLayout, viewportBand: ShellViewportBand): 1120 | 1280 | 1400 | 1440 | 1760 | 1920 | undefined;
|
|
3
|
+
export declare function getResolvedShellPanelWidth({ preferredWidth, viewportWidth, sidebarOpen, sidebarWidth, }: {
|
|
4
|
+
preferredWidth: number;
|
|
5
|
+
viewportWidth: number;
|
|
6
|
+
sidebarOpen: boolean;
|
|
7
|
+
sidebarWidth: number;
|
|
8
|
+
}): number;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const DESKTOP_APP_RAIL_WIDTH = 80;
|
|
2
|
+
const DESKTOP_SECTION_SIDEBAR_TOGGLE_WIDTH = 20;
|
|
3
|
+
const DESKTOP_SECTION_SIDEBAR_RESIZE_WIDTH = 2;
|
|
4
|
+
const DESKTOP_PANEL_HANDLE_WIDTH = 8;
|
|
5
|
+
const DESKTOP_PANEL_GUTTER = 16;
|
|
6
|
+
const SIDE_PANEL_MIN_WIDTH = 280;
|
|
7
|
+
const SIDE_PANEL_MAX_WIDTH = 520;
|
|
8
|
+
export function getShellContentMaxWidth(layout, viewportBand) {
|
|
9
|
+
if (layout === 'workspace') {
|
|
10
|
+
if (viewportBand === 'desktop')
|
|
11
|
+
return 1120;
|
|
12
|
+
if (viewportBand === 'wide')
|
|
13
|
+
return 1280;
|
|
14
|
+
if (viewportBand === 'ultrawide')
|
|
15
|
+
return 1400;
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (layout === 'fullbleed') {
|
|
19
|
+
if (viewportBand === 'desktop')
|
|
20
|
+
return 1440;
|
|
21
|
+
if (viewportBand === 'wide')
|
|
22
|
+
return 1760;
|
|
23
|
+
if (viewportBand === 'ultrawide')
|
|
24
|
+
return 1920;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
export function getResolvedShellPanelWidth({ preferredWidth, viewportWidth, sidebarOpen, sidebarWidth, }) {
|
|
30
|
+
const leftNavigationWidth = DESKTOP_APP_RAIL_WIDTH +
|
|
31
|
+
DESKTOP_SECTION_SIDEBAR_TOGGLE_WIDTH +
|
|
32
|
+
(sidebarOpen ? sidebarWidth + DESKTOP_SECTION_SIDEBAR_RESIZE_WIDTH : 0);
|
|
33
|
+
const clampedPreferredWidth = Math.min(SIDE_PANEL_MAX_WIDTH, Math.max(SIDE_PANEL_MIN_WIDTH, preferredWidth));
|
|
34
|
+
const maxWidthForViewport = viewportWidth - leftNavigationWidth - DESKTOP_PANEL_GUTTER * 2 - DESKTOP_PANEL_HANDLE_WIDTH;
|
|
35
|
+
if (!Number.isFinite(maxWidthForViewport))
|
|
36
|
+
return clampedPreferredWidth;
|
|
37
|
+
return Math.max(SIDE_PANEL_MIN_WIDTH, Math.min(clampedPreferredWidth, Math.floor(maxWidthForViewport)));
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ShellModuleDefinition, ShellNavItem } from './types';
|
|
3
|
+
interface ShellModuleNavRenderArgs {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className: string;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
item: ShellNavItem;
|
|
8
|
+
}
|
|
9
|
+
export interface ShellModuleNavProps {
|
|
10
|
+
module: Pick<ShellModuleDefinition, 'description' | 'items' | 'label'>;
|
|
11
|
+
ariaLabel: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
descriptionClassName?: string;
|
|
14
|
+
headerClassName?: string;
|
|
15
|
+
itemClassName?: string;
|
|
16
|
+
activeItemClassName?: string;
|
|
17
|
+
inactiveItemClassName?: string;
|
|
18
|
+
itemsClassName?: string;
|
|
19
|
+
itemIconClassName?: string;
|
|
20
|
+
itemIconSize?: number;
|
|
21
|
+
itemLabelClassName?: string;
|
|
22
|
+
itemDescriptionClassName?: string;
|
|
23
|
+
itemShortcutClassName?: string;
|
|
24
|
+
titleClassName?: string;
|
|
25
|
+
isItemActive: (item: ShellNavItem) => boolean;
|
|
26
|
+
renderLink: (args: ShellModuleNavRenderArgs) => ReactNode;
|
|
27
|
+
}
|
|
28
|
+
export declare function ShellModuleNav({ module, ariaLabel, className, descriptionClassName, headerClassName, itemClassName, activeItemClassName, inactiveItemClassName, itemsClassName, itemIconClassName, itemIconSize, itemLabelClassName, itemDescriptionClassName, itemShortcutClassName, titleClassName, isItemActive, renderLink, }: ShellModuleNavProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
function joinClasses(...values) {
|
|
3
|
+
return values.filter(Boolean).join(' ');
|
|
4
|
+
}
|
|
5
|
+
export function ShellModuleNav({ module, ariaLabel, className, descriptionClassName, headerClassName, itemClassName, activeItemClassName, inactiveItemClassName, itemsClassName, itemIconClassName, itemIconSize = 18, itemLabelClassName, itemDescriptionClassName, itemShortcutClassName, titleClassName, isItemActive, renderLink, }) {
|
|
6
|
+
return (_jsxs("div", { className: className, children: [_jsxs("div", { className: headerClassName, children: [_jsx("div", { className: titleClassName, children: module.label }), module.description ? (_jsx("div", { className: descriptionClassName, children: module.description })) : null] }), _jsx("nav", { "aria-label": ariaLabel, className: itemsClassName, children: module.items.map((item) => {
|
|
7
|
+
const isActive = isItemActive(item);
|
|
8
|
+
const Icon = item.icon;
|
|
9
|
+
return renderLink({
|
|
10
|
+
item,
|
|
11
|
+
isActive,
|
|
12
|
+
className: joinClasses(itemClassName, isActive ? activeItemClassName : inactiveItemClassName),
|
|
13
|
+
children: (_jsxs(_Fragment, { children: [_jsx(Icon, { size: itemIconSize, className: itemIconClassName }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: itemLabelClassName, children: item.label }), item.description ? (_jsx("span", { className: itemDescriptionClassName, children: item.description })) : null] }), item.shortcut ? (_jsx("kbd", { className: itemShortcutClassName, children: item.shortcut })) : null] })),
|
|
14
|
+
});
|
|
15
|
+
}) })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ShellPanelDefinition } from './types';
|
|
2
|
+
export interface ShellPanelRailProps {
|
|
3
|
+
moduleViews: ShellPanelDefinition[];
|
|
4
|
+
globalViews: ShellPanelDefinition[];
|
|
5
|
+
activePanelView: string | null;
|
|
6
|
+
className?: string;
|
|
7
|
+
onTogglePanelView: (viewId: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function ShellPanelRail({ moduleViews, globalViews, activePanelView, className, onTogglePanelView, }: ShellPanelRailProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function joinClasses(...values) {
|
|
3
|
+
return values.filter(Boolean).join(' ');
|
|
4
|
+
}
|
|
5
|
+
function PanelRailButtons({ views, activePanelView, onTogglePanelView, }) {
|
|
6
|
+
return (_jsx("div", { className: "flex w-full flex-col items-center gap-1", children: views.map(({ id, label, icon: Icon }) => {
|
|
7
|
+
const isActive = activePanelView === id;
|
|
8
|
+
return (_jsxs("button", { type: "button", "aria-label": label, "aria-pressed": isActive, onClick: () => onTogglePanelView(id), className: joinClasses('flex w-full flex-col items-center gap-1 rounded-xl px-1 py-2 text-[var(--app-rail-foreground)] transition-colors', isActive
|
|
9
|
+
? 'bg-[var(--app-rail-item-active-bg)] text-[var(--foreground)]'
|
|
10
|
+
: 'hover:bg-[var(--surface-highlight)] hover:text-[var(--foreground)]'), children: [_jsx(Icon, { size: 16 }), _jsx("span", { className: "text-[9px] font-medium leading-none", children: label })] }, id));
|
|
11
|
+
}) }));
|
|
12
|
+
}
|
|
13
|
+
export function ShellPanelRail({ moduleViews, globalViews, activePanelView, className, onTogglePanelView, }) {
|
|
14
|
+
if (moduleViews.length === 0 && globalViews.length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
return (_jsxs("aside", { "aria-label": "Panel rail", className: joinClasses('flex h-full w-[var(--right-rail-width)] shrink-0 flex-col items-center bg-[var(--app-rail)] px-1.5 py-3', className), children: [_jsx(PanelRailButtons, { views: moduleViews, activePanelView: activePanelView, onTogglePanelView: onTogglePanelView }), _jsx("div", { className: "grow" }), _jsx(PanelRailButtons, { views: globalViews, activePanelView: activePanelView, onTogglePanelView: onTogglePanelView })] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ShellContentLayout, ShellTab } from './types';
|
|
3
|
+
interface ShellRouteMatchLike {
|
|
4
|
+
handle?: unknown;
|
|
5
|
+
params?: Record<string, string | undefined>;
|
|
6
|
+
}
|
|
7
|
+
export declare function getShellContentLayoutFromMatches(matches: ShellRouteMatchLike[]): ShellContentLayout;
|
|
8
|
+
export declare function getShellTabsFromMatches(matches: ShellRouteMatchLike[], pathname: string): ShellTab[];
|
|
9
|
+
export declare function getShellActionsFromMatches(matches: ShellRouteMatchLike[], pathname: string): ReactNode;
|
|
10
|
+
export declare function getShellPanelViewsFromMatches(matches: ShellRouteMatchLike[]): string[] | undefined;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function getLastMatch(matches) {
|
|
2
|
+
return matches[matches.length - 1];
|
|
3
|
+
}
|
|
4
|
+
function getRouteHandle(match) {
|
|
5
|
+
return match?.handle;
|
|
6
|
+
}
|
|
7
|
+
export function getShellContentLayoutFromMatches(matches) {
|
|
8
|
+
return getRouteHandle(getLastMatch(matches))?.shellContentLayout ?? 'workspace';
|
|
9
|
+
}
|
|
10
|
+
export function getShellTabsFromMatches(matches, pathname) {
|
|
11
|
+
const lastMatch = getLastMatch(matches);
|
|
12
|
+
const shellTabs = getRouteHandle(lastMatch)?.shellTabs;
|
|
13
|
+
if (!shellTabs)
|
|
14
|
+
return [];
|
|
15
|
+
if (typeof shellTabs === 'function') {
|
|
16
|
+
return shellTabs({ params: lastMatch.params ?? {}, pathname });
|
|
17
|
+
}
|
|
18
|
+
return shellTabs;
|
|
19
|
+
}
|
|
20
|
+
export function getShellActionsFromMatches(matches, pathname) {
|
|
21
|
+
const lastMatch = getLastMatch(matches);
|
|
22
|
+
const shellActions = getRouteHandle(lastMatch)?.shellActions;
|
|
23
|
+
if (!shellActions)
|
|
24
|
+
return null;
|
|
25
|
+
if (typeof shellActions === 'function') {
|
|
26
|
+
return shellActions({ params: lastMatch.params ?? {}, pathname });
|
|
27
|
+
}
|
|
28
|
+
return shellActions;
|
|
29
|
+
}
|
|
30
|
+
export function getShellPanelViewsFromMatches(matches) {
|
|
31
|
+
return getRouteHandle(getLastMatch(matches))?.shellPanelViews;
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ShellContentLayout } from './types';
|
|
3
|
+
export interface ShellScrollableContentProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
desktopDockOffset?: number;
|
|
6
|
+
layout: ShellContentLayout;
|
|
7
|
+
maxWidth?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function ShellScrollableContent({ children, desktopDockOffset, layout, maxWidth, }: ShellScrollableContentProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
function joinClasses(...values) {
|
|
3
|
+
return values.filter(Boolean).join(' ');
|
|
4
|
+
}
|
|
5
|
+
export function ShellScrollableContent({ children, desktopDockOffset = 0, layout, maxWidth, }) {
|
|
6
|
+
return (_jsx("main", { "data-testid": "shell-workspace-viewport", className: "shell-scrollbar-hidden absolute inset-0 min-w-0 overflow-y-auto overflow-x-hidden pb-[var(--bottom-nav-height)] transition-[padding] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] md:pb-0", style: desktopDockOffset > 0 ? { paddingRight: `${desktopDockOffset}px` } : undefined, children: layout === 'centered' ? (_jsx("div", { "data-testid": "shell-content-centered", className: "flex min-h-full items-center justify-center px-5 py-5 sm:px-6 sm:py-6 md:px-8 md:py-8 lg:px-10", children: _jsx("div", { className: "flex w-full max-w-3xl items-center justify-center", children: children }) })) : layout === 'fullbleed' ? (_jsx("div", { "data-testid": "shell-content-fullbleed", className: "mx-auto w-full px-5 py-5 sm:px-6 sm:py-6 md:px-8 md:py-8 lg:px-10", style: maxWidth ? { maxWidth: `${maxWidth}px` } : undefined, children: children })) : (_jsx("div", { "data-testid": "shell-content-workspace", className: joinClasses('mx-auto w-full px-5 py-5 sm:px-6 sm:py-6 md:px-8 md:py-8 lg:px-10'), style: maxWidth ? { maxWidth: `${maxWidth}px` } : undefined, children: children })) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare const DEFAULT_SIDE_PANEL_WIDTH = 360;
|
|
3
|
+
export declare const MIN_SIDE_PANEL_WIDTH = 280;
|
|
4
|
+
export declare const MAX_SIDE_PANEL_WIDTH = 520;
|
|
5
|
+
export interface ShellSearchParamsAdapter {
|
|
6
|
+
searchParams: URLSearchParams;
|
|
7
|
+
setSearchParams: (updater: (currentSearchParams: URLSearchParams) => URLSearchParams) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface ShellStateContextValue {
|
|
10
|
+
activePanelView: string | null;
|
|
11
|
+
lastPanelView: string | null;
|
|
12
|
+
setActivePanelView: (view: string | null) => void;
|
|
13
|
+
closePanelView: () => void;
|
|
14
|
+
restoreLastPanelView: (fallbackView: string | null) => void;
|
|
15
|
+
togglePanelView: (viewId: string) => void;
|
|
16
|
+
sidePanelWidth: number;
|
|
17
|
+
setSidePanelWidth: (width: number) => void;
|
|
18
|
+
activeRail: string;
|
|
19
|
+
setActiveRail: (rail: string) => void;
|
|
20
|
+
selectedEntityId: string | null;
|
|
21
|
+
selectEntity: (entityId: string, options?: {
|
|
22
|
+
panelView?: string | null;
|
|
23
|
+
}) => void;
|
|
24
|
+
clearEntity: () => void;
|
|
25
|
+
commandPaletteOpen: boolean;
|
|
26
|
+
setCommandPaletteOpen: (open: boolean) => void;
|
|
27
|
+
openCommandPalette: () => void;
|
|
28
|
+
closeCommandPalette: () => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function clampSidePanelWidth(width: number): number;
|
|
31
|
+
export declare function ShellStateProvider({ children, adapter, initialActiveRail, panelQueryParam, selectionQueryParam, sidePanelWidthStorageKey, }: {
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
adapter: ShellSearchParamsAdapter;
|
|
34
|
+
initialActiveRail?: string;
|
|
35
|
+
panelQueryParam?: string;
|
|
36
|
+
selectionQueryParam?: string;
|
|
37
|
+
sidePanelWidthStorageKey?: string;
|
|
38
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export declare function useShellState(): ShellStateContextValue;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
export const DEFAULT_SIDE_PANEL_WIDTH = 360;
|
|
4
|
+
export const MIN_SIDE_PANEL_WIDTH = 280;
|
|
5
|
+
export const MAX_SIDE_PANEL_WIDTH = 520;
|
|
6
|
+
const ShellStateContext = createContext(null);
|
|
7
|
+
export function clampSidePanelWidth(width) {
|
|
8
|
+
return Math.min(MAX_SIDE_PANEL_WIDTH, Math.max(MIN_SIDE_PANEL_WIDTH, width));
|
|
9
|
+
}
|
|
10
|
+
function readStoredSidePanelWidth(storageKey) {
|
|
11
|
+
if (typeof window === 'undefined')
|
|
12
|
+
return DEFAULT_SIDE_PANEL_WIDTH;
|
|
13
|
+
const storedValue = typeof window.localStorage?.getItem === 'function' ? window.localStorage.getItem(storageKey) : null;
|
|
14
|
+
const stored = Number(storedValue);
|
|
15
|
+
return Number.isFinite(stored) && stored > 0
|
|
16
|
+
? clampSidePanelWidth(stored)
|
|
17
|
+
: DEFAULT_SIDE_PANEL_WIDTH;
|
|
18
|
+
}
|
|
19
|
+
export function ShellStateProvider({ children, adapter, initialActiveRail = 'home', panelQueryParam = 'panel', selectionQueryParam = 'selection', sidePanelWidthStorageKey = 'meda.side-panel.width', }) {
|
|
20
|
+
const { searchParams, setSearchParams } = adapter;
|
|
21
|
+
const [sidePanelWidthState, setSidePanelWidthState] = useState(() => readStoredSidePanelWidth(sidePanelWidthStorageKey));
|
|
22
|
+
const [activeRail, setActiveRail] = useState(initialActiveRail);
|
|
23
|
+
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
24
|
+
const lastPanelViewRef = useRef(null);
|
|
25
|
+
const persistTimerRef = useRef(undefined);
|
|
26
|
+
const activePanelView = searchParams.get(panelQueryParam);
|
|
27
|
+
const selectedEntityId = searchParams.get(selectionQueryParam);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (activePanelView) {
|
|
30
|
+
lastPanelViewRef.current = activePanelView;
|
|
31
|
+
}
|
|
32
|
+
}, [activePanelView]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
return () => {
|
|
35
|
+
clearTimeout(persistTimerRef.current);
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
function updateSearchParams(mutate) {
|
|
39
|
+
setSearchParams((currentSearchParams) => {
|
|
40
|
+
const nextSearchParams = new URLSearchParams(currentSearchParams);
|
|
41
|
+
mutate(nextSearchParams);
|
|
42
|
+
return nextSearchParams;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function setActivePanelView(view) {
|
|
46
|
+
if (view) {
|
|
47
|
+
lastPanelViewRef.current = view;
|
|
48
|
+
}
|
|
49
|
+
updateSearchParams((nextSearchParams) => {
|
|
50
|
+
if (view) {
|
|
51
|
+
nextSearchParams.set(panelQueryParam, view);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
nextSearchParams.delete(panelQueryParam);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function closePanelView() {
|
|
59
|
+
updateSearchParams((nextSearchParams) => {
|
|
60
|
+
nextSearchParams.delete(panelQueryParam);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function restoreLastPanelView(fallbackView) {
|
|
64
|
+
const nextView = lastPanelViewRef.current ?? fallbackView;
|
|
65
|
+
if (!nextView)
|
|
66
|
+
return;
|
|
67
|
+
setActivePanelView(nextView);
|
|
68
|
+
}
|
|
69
|
+
function setSidePanelWidth(width) {
|
|
70
|
+
const nextWidth = clampSidePanelWidth(width);
|
|
71
|
+
setSidePanelWidthState(nextWidth);
|
|
72
|
+
clearTimeout(persistTimerRef.current);
|
|
73
|
+
persistTimerRef.current = setTimeout(() => {
|
|
74
|
+
if (typeof window !== 'undefined' && typeof window.localStorage?.setItem === 'function') {
|
|
75
|
+
window.localStorage.setItem(sidePanelWidthStorageKey, String(nextWidth));
|
|
76
|
+
}
|
|
77
|
+
}, 300);
|
|
78
|
+
}
|
|
79
|
+
function togglePanelView(viewId) {
|
|
80
|
+
if (activePanelView === viewId) {
|
|
81
|
+
closePanelView();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
setActivePanelView(viewId);
|
|
85
|
+
}
|
|
86
|
+
function selectEntity(entityId, options) {
|
|
87
|
+
updateSearchParams((nextSearchParams) => {
|
|
88
|
+
nextSearchParams.set(selectionQueryParam, entityId);
|
|
89
|
+
if (options?.panelView) {
|
|
90
|
+
nextSearchParams.set(panelQueryParam, options.panelView);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function clearEntity() {
|
|
95
|
+
updateSearchParams((nextSearchParams) => {
|
|
96
|
+
nextSearchParams.delete(selectionQueryParam);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function openCommandPalette() {
|
|
100
|
+
setCommandPaletteOpen(true);
|
|
101
|
+
}
|
|
102
|
+
function closeCommandPalette() {
|
|
103
|
+
setCommandPaletteOpen(false);
|
|
104
|
+
}
|
|
105
|
+
const value = useMemo(() => ({
|
|
106
|
+
activePanelView,
|
|
107
|
+
lastPanelView: lastPanelViewRef.current,
|
|
108
|
+
setActivePanelView,
|
|
109
|
+
closePanelView,
|
|
110
|
+
restoreLastPanelView,
|
|
111
|
+
togglePanelView,
|
|
112
|
+
sidePanelWidth: sidePanelWidthState,
|
|
113
|
+
setSidePanelWidth,
|
|
114
|
+
activeRail,
|
|
115
|
+
setActiveRail,
|
|
116
|
+
selectedEntityId,
|
|
117
|
+
selectEntity,
|
|
118
|
+
clearEntity,
|
|
119
|
+
commandPaletteOpen,
|
|
120
|
+
setCommandPaletteOpen,
|
|
121
|
+
openCommandPalette,
|
|
122
|
+
closeCommandPalette,
|
|
123
|
+
}), [activePanelView, activeRail, commandPaletteOpen, selectedEntityId, sidePanelWidthState]);
|
|
124
|
+
return _jsx(ShellStateContext.Provider, { value: value, children: children });
|
|
125
|
+
}
|
|
126
|
+
export function useShellState() {
|
|
127
|
+
const context = useContext(ShellStateContext);
|
|
128
|
+
if (!context) {
|
|
129
|
+
throw new Error('useShellState must be used within a ShellStateProvider');
|
|
130
|
+
}
|
|
131
|
+
return context;
|
|
132
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ShellTab } from './types';
|
|
3
|
+
interface ShellTabBarRenderArgs {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className: string;
|
|
6
|
+
isActive: boolean;
|
|
7
|
+
tab: ShellTab;
|
|
8
|
+
}
|
|
9
|
+
export interface ShellTabBarProps {
|
|
10
|
+
tabs: ShellTab[];
|
|
11
|
+
activeTab?: string;
|
|
12
|
+
onTabChange?: (tabId: string) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
renderLink?: (args: ShellTabBarRenderArgs) => ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function ShellTabBar({ tabs, activeTab, onTabChange, className, ariaLabel, renderLink, }: ShellTabBarProps): import("react/jsx-runtime").JSX.Element | null;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
function joinClasses(...values) {
|
|
3
|
+
return values.filter(Boolean).join(' ');
|
|
4
|
+
}
|
|
5
|
+
const tabBarClassName = 'flex h-[var(--tab-bar-height)] shrink-0 items-center gap-3 overflow-x-auto bg-[var(--background)] px-3';
|
|
6
|
+
const tabClassName = 'border-b-[1.5px] pb-2.5 pt-2.5 text-xs whitespace-nowrap transition-colors';
|
|
7
|
+
const activeTabClassName = 'border-[var(--primary)] text-[var(--primary)]';
|
|
8
|
+
const inactiveTabClassName = 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]';
|
|
9
|
+
export function ShellTabBar({ tabs, activeTab, onTabChange, className, ariaLabel = 'Tabs', renderLink, }) {
|
|
10
|
+
if (tabs.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
if (renderLink && tabs.every((tab) => Boolean(tab.to))) {
|
|
13
|
+
return (_jsx("nav", { "aria-label": ariaLabel, className: joinClasses(tabBarClassName, className), children: tabs.map((tab) => {
|
|
14
|
+
const isActive = tab.id === activeTab;
|
|
15
|
+
return renderLink({
|
|
16
|
+
tab,
|
|
17
|
+
isActive,
|
|
18
|
+
className: joinClasses(tabClassName, isActive ? activeTabClassName : inactiveTabClassName),
|
|
19
|
+
children: tab.label,
|
|
20
|
+
});
|
|
21
|
+
}) }));
|
|
22
|
+
}
|
|
23
|
+
return (_jsx("div", { "aria-label": ariaLabel, className: joinClasses(tabBarClassName, className), children: tabs.map((tab) => {
|
|
24
|
+
const isActive = tab.id === activeTab;
|
|
25
|
+
return (_jsx("button", { type: "button", "data-active": isActive, onClick: () => onTabChange?.(tab.id), className: joinClasses(tabClassName, isActive ? activeTabClassName : inactiveTabClassName), children: tab.label }, tab.id));
|
|
26
|
+
}) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
export type ShellContentLayout = 'workspace' | 'centered' | 'fullbleed';
|
|
4
|
+
export type ShellViewportBand = 'mobile' | 'tablet' | 'desktop' | 'wide' | 'ultrawide';
|
|
5
|
+
export interface ShellTab {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
to?: string;
|
|
9
|
+
icon?: LucideIcon;
|
|
10
|
+
}
|
|
11
|
+
export interface ShellNavItem {
|
|
12
|
+
to: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
icon: LucideIcon;
|
|
16
|
+
shortcut?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ShellRailItem {
|
|
19
|
+
to: string;
|
|
20
|
+
label: string;
|
|
21
|
+
icon: LucideIcon;
|
|
22
|
+
}
|
|
23
|
+
export interface ShellModuleDefinition {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
description: string;
|
|
27
|
+
items: ShellNavItem[];
|
|
28
|
+
headerTabs?: ShellTab[];
|
|
29
|
+
}
|
|
30
|
+
export interface ShellPanelDefinition {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
icon: LucideIcon;
|
|
34
|
+
global?: boolean;
|
|
35
|
+
hidden?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface ShellCommandDefinition {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
to: string;
|
|
41
|
+
icon: LucideIcon;
|
|
42
|
+
group: string;
|
|
43
|
+
shortcut?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ShellRenderContext {
|
|
46
|
+
sectionKey: string;
|
|
47
|
+
viewId: string;
|
|
48
|
+
selectedProductId: string | null;
|
|
49
|
+
}
|
|
50
|
+
export type ShellPanelRenderer = (context: ShellRenderContext) => ReactNode;
|
|
51
|
+
export interface ShellRouteContext {
|
|
52
|
+
pathname: string;
|
|
53
|
+
params: Record<string, string | undefined>;
|
|
54
|
+
}
|
|
55
|
+
export interface ShellHostAdapter {
|
|
56
|
+
route: ShellRouteContext;
|
|
57
|
+
navigate: (to: string) => void;
|
|
58
|
+
}
|
|
59
|
+
export interface ShellViewDefinition {
|
|
60
|
+
shellContentLayout?: ShellContentLayout;
|
|
61
|
+
shellTabs?: ShellTab[] | ((context: ShellRouteContext) => ShellTab[]);
|
|
62
|
+
shellActions?: ReactNode | ((context: ShellRouteContext) => ReactNode);
|
|
63
|
+
shellPanelViews?: string[];
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ShellCommandDefinition, ShellModuleDefinition, ShellPanelDefinition } from './types';
|
|
2
|
+
export interface ShellPanelCollection {
|
|
3
|
+
globalViews: ShellPanelDefinition[];
|
|
4
|
+
moduleViews: ShellPanelDefinition[];
|
|
5
|
+
views: ShellPanelDefinition[];
|
|
6
|
+
}
|
|
7
|
+
export declare function buildShellShortcutMap(modules: Record<string, ShellModuleDefinition>): Map<string, string>;
|
|
8
|
+
export declare function buildShellSectionCommands(module: Pick<ShellModuleDefinition, 'id' | 'items'> | null, group?: string): ShellCommandDefinition[];
|
|
9
|
+
export declare function getShellPanelCollection({ globalPanelViews, panelViews, productPanelViews, sectionKey, selectedProductId, }: {
|
|
10
|
+
globalPanelViews?: ShellPanelDefinition[];
|
|
11
|
+
panelViews: Record<string, ShellPanelDefinition[]>;
|
|
12
|
+
productPanelViews?: ShellPanelDefinition[];
|
|
13
|
+
sectionKey: string;
|
|
14
|
+
selectedProductId?: string | null;
|
|
15
|
+
}): ShellPanelCollection;
|
|
16
|
+
export declare function getDefaultShellPanelView(options: {
|
|
17
|
+
globalPanelViews?: ShellPanelDefinition[];
|
|
18
|
+
panelViews: Record<string, ShellPanelDefinition[]>;
|
|
19
|
+
productPanelViews?: ShellPanelDefinition[];
|
|
20
|
+
sectionKey: string;
|
|
21
|
+
selectedProductId?: string | null;
|
|
22
|
+
}): string;
|
|
23
|
+
export declare function isShellPanelViewAvailable(options: {
|
|
24
|
+
globalPanelViews?: ShellPanelDefinition[];
|
|
25
|
+
panelViews: Record<string, ShellPanelDefinition[]>;
|
|
26
|
+
productPanelViews?: ShellPanelDefinition[];
|
|
27
|
+
sectionKey: string;
|
|
28
|
+
selectedProductId?: string | null;
|
|
29
|
+
}, viewId: string | null): boolean;
|
|
30
|
+
export declare function resolveShellPanelView(options: {
|
|
31
|
+
globalPanelViews?: ShellPanelDefinition[];
|
|
32
|
+
panelViews: Record<string, ShellPanelDefinition[]>;
|
|
33
|
+
productPanelViews?: ShellPanelDefinition[];
|
|
34
|
+
sectionKey: string;
|
|
35
|
+
selectedProductId?: string | null;
|
|
36
|
+
}, viewId: string | null): string | null;
|
|
37
|
+
export declare function getShellPanelView(options: {
|
|
38
|
+
globalPanelViews?: ShellPanelDefinition[];
|
|
39
|
+
panelViews: Record<string, ShellPanelDefinition[]>;
|
|
40
|
+
productPanelViews?: ShellPanelDefinition[];
|
|
41
|
+
sectionKey: string;
|
|
42
|
+
selectedProductId?: string | null;
|
|
43
|
+
}, viewId: string | null): ShellPanelDefinition | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function buildShellShortcutMap(modules) {
|
|
2
|
+
const map = new Map();
|
|
3
|
+
for (const section of Object.values(modules)) {
|
|
4
|
+
for (const item of section.items) {
|
|
5
|
+
if (item.shortcut) {
|
|
6
|
+
map.set(item.shortcut, item.to);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return map;
|
|
11
|
+
}
|
|
12
|
+
export function buildShellSectionCommands(module, group = 'Current Section') {
|
|
13
|
+
if (!module)
|
|
14
|
+
return [];
|
|
15
|
+
return module.items.map((item) => ({
|
|
16
|
+
id: `${module.id}:${item.to}`,
|
|
17
|
+
label: item.label,
|
|
18
|
+
to: item.to,
|
|
19
|
+
icon: item.icon,
|
|
20
|
+
group,
|
|
21
|
+
shortcut: item.shortcut,
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
export function getShellPanelCollection({ globalPanelViews = [], panelViews, productPanelViews = [], sectionKey, selectedProductId = null, }) {
|
|
25
|
+
const moduleViews = selectedProductId ? productPanelViews : (panelViews[sectionKey] ?? []);
|
|
26
|
+
const globalViews = globalPanelViews;
|
|
27
|
+
return {
|
|
28
|
+
moduleViews,
|
|
29
|
+
globalViews,
|
|
30
|
+
views: [...moduleViews, ...globalViews],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function getDefaultShellPanelView(options) {
|
|
34
|
+
return getShellPanelCollection(options).views[0]?.id ?? null;
|
|
35
|
+
}
|
|
36
|
+
export function isShellPanelViewAvailable(options, viewId) {
|
|
37
|
+
if (!viewId)
|
|
38
|
+
return false;
|
|
39
|
+
return getShellPanelCollection(options).views.some((view) => view.id === viewId);
|
|
40
|
+
}
|
|
41
|
+
export function resolveShellPanelView(options, viewId) {
|
|
42
|
+
if (!viewId)
|
|
43
|
+
return null;
|
|
44
|
+
return isShellPanelViewAvailable(options, viewId) ? viewId : getDefaultShellPanelView(options);
|
|
45
|
+
}
|
|
46
|
+
export function getShellPanelView(options, viewId) {
|
|
47
|
+
if (!viewId)
|
|
48
|
+
return null;
|
|
49
|
+
return getShellPanelCollection(options).views.find((view) => view.id === viewId) ?? null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ShellViewportBand } from './types';
|
|
3
|
+
export interface WorkbenchLayoutProps {
|
|
4
|
+
viewportBand: ShellViewportBand;
|
|
5
|
+
main: ReactNode;
|
|
6
|
+
aside?: ReactNode;
|
|
7
|
+
toolbar?: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function WorkbenchLayout({ viewportBand, main, aside, toolbar, className, }: WorkbenchLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function joinClasses(...values) {
|
|
3
|
+
return values.filter(Boolean).join(' ');
|
|
4
|
+
}
|
|
5
|
+
function getWorkbenchMaxWidth(viewportBand) {
|
|
6
|
+
if (viewportBand === 'desktop')
|
|
7
|
+
return 1440;
|
|
8
|
+
if (viewportBand === 'wide')
|
|
9
|
+
return 1760;
|
|
10
|
+
if (viewportBand === 'ultrawide')
|
|
11
|
+
return 1920;
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
export function WorkbenchLayout({ viewportBand, main, aside, toolbar, className, }) {
|
|
15
|
+
const maxWidth = getWorkbenchMaxWidth(viewportBand);
|
|
16
|
+
const layout = !aside
|
|
17
|
+
? 'single'
|
|
18
|
+
: viewportBand === 'mobile' || viewportBand === 'tablet'
|
|
19
|
+
? 'stacked'
|
|
20
|
+
: viewportBand === 'desktop'
|
|
21
|
+
? 'split'
|
|
22
|
+
: 'multi-zone';
|
|
23
|
+
const columnStyle = layout === 'split'
|
|
24
|
+
? { gridTemplateColumns: 'minmax(0, 1fr) minmax(280px, 320px)' }
|
|
25
|
+
: layout === 'multi-zone'
|
|
26
|
+
? { gridTemplateColumns: 'minmax(0, 1fr) minmax(300px, 360px)' }
|
|
27
|
+
: undefined;
|
|
28
|
+
return (_jsxs("section", { "data-testid": "workbench-layout", "data-band": viewportBand, className: joinClasses('mx-auto flex w-full flex-col gap-6', className), style: maxWidth ? { maxWidth: `${maxWidth}px` } : undefined, children: [toolbar ? _jsx("div", { "data-testid": "workbench-toolbar", children: toolbar }) : null, _jsxs("div", { "data-testid": "workbench-columns", "data-layout": layout, className: joinClasses('w-full gap-5', layout === 'stacked' ? 'flex flex-col' : layout === 'single' ? 'flex flex-col' : 'grid'), style: columnStyle, children: [_jsx("div", { "data-testid": "workbench-main", className: "min-w-0", children: main }), aside ? (_jsx("aside", { "data-testid": "workbench-aside", className: "min-w-0", children: aside })) : null] })] }));
|
|
29
|
+
}
|
package/dist/styles.css
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@medalsocial/meda",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Meda UI shell and runtime package.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"packageManager": "pnpm@10.28.0",
|
|
10
|
+
"files": [
|
|
11
|
+
"README.md",
|
|
12
|
+
"components.json",
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./shell": {
|
|
21
|
+
"types": "./dist/shell/index.d.ts",
|
|
22
|
+
"default": "./dist/shell/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./styles.css": "./dist/styles.css"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "pnpm exec tsc -p tsconfig.build.json && node ./scripts/build.mjs",
|
|
28
|
+
"prepack": "pnpm build",
|
|
29
|
+
"test": "vitest run --environment jsdom",
|
|
30
|
+
"test:run": "vitest run --environment jsdom",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"version": "changeset version",
|
|
33
|
+
"release": "pnpm build && pnpm publish --access public --no-git-checks"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=19",
|
|
37
|
+
"react-dom": ">=19"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@base-ui/react": "^1.4.0",
|
|
41
|
+
"@fontsource-variable/geist": "^5.2.8",
|
|
42
|
+
"@fontsource-variable/geist-mono": "^5.2.7",
|
|
43
|
+
"class-variance-authority": "^0.7.1",
|
|
44
|
+
"clsx": "^2.1.1",
|
|
45
|
+
"cmdk": "^1.1.1",
|
|
46
|
+
"lucide-react": "^1.8.0",
|
|
47
|
+
"react-resizable-panels": "^4.10.0",
|
|
48
|
+
"tailwind-merge": "^3.5.0",
|
|
49
|
+
"vaul": "^1.1.2"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@changesets/changelog-github": "^0.5.1",
|
|
53
|
+
"@changesets/cli": "^2.29.0",
|
|
54
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
55
|
+
"@testing-library/react": "^16.3.2",
|
|
56
|
+
"@types/react": "^19.2.14",
|
|
57
|
+
"@types/react-dom": "^19.2.3",
|
|
58
|
+
"jsdom": "^29.0.2",
|
|
59
|
+
"typescript": "~6.0.2",
|
|
60
|
+
"vitest": "^4.1.4"
|
|
61
|
+
}
|
|
62
|
+
}
|