@medalsocial/meda 1.1.2 → 1.3.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/dist/components/ui/checkbox.d.ts +8 -0
- package/dist/components/ui/checkbox.js +9 -0
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/collapsible.js +13 -0
- package/dist/kanban/index.d.ts +5 -0
- package/dist/kanban/index.js +4 -0
- package/dist/kanban/kanban-board.d.ts +8 -0
- package/dist/kanban/kanban-board.js +197 -0
- package/dist/kanban/kanban-card-wrapper.d.ts +16 -0
- package/dist/kanban/kanban-card-wrapper.js +20 -0
- package/dist/kanban/kanban-collision.d.ts +16 -0
- package/dist/kanban/kanban-collision.js +18 -0
- package/dist/kanban/kanban-column.d.ts +19 -0
- package/dist/kanban/kanban-column.js +19 -0
- package/dist/kanban/kanban-drop-handler.d.ts +16 -0
- package/dist/kanban/kanban-drop-handler.js +84 -0
- package/dist/kanban/types.d.ts +47 -0
- package/dist/kanban/types.js +7 -0
- package/dist/list/index.d.ts +2 -0
- package/dist/list/index.js +1 -0
- package/dist/list/list-row.d.ts +52 -0
- package/dist/list/list-row.js +37 -0
- package/dist/shell/app-shell-workspace.js +11 -6
- package/dist/shell/command-palette.js +2 -1
- package/dist/shell/context-rail.d.ts +1 -1
- package/dist/shell/context-rail.js +5 -4
- package/dist/shell/drag-mode-banner.d.ts +31 -0
- package/dist/shell/drag-mode-banner.js +19 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +5 -0
- package/dist/shell/internal/mobile-drawers.d.ts +8 -2
- package/dist/shell/internal/mobile-drawers.js +40 -12
- package/dist/shell/panel-views-provider.d.ts +15 -0
- package/dist/shell/panel-views-provider.js +56 -0
- package/dist/shell/rail-drop-slot.d.ts +41 -0
- package/dist/shell/rail-drop-slot.js +49 -0
- package/dist/shell/rail-drop-zones.d.ts +28 -0
- package/dist/shell/rail-drop-zones.js +17 -0
- package/dist/shell/right-panel.js +21 -8
- package/dist/shell/shell-provider.d.ts +14 -1
- package/dist/shell/shell-provider.js +64 -2
- package/dist/shell/types.d.ts +5 -1
- package/dist/timeline/index.d.ts +3 -0
- package/dist/timeline/index.js +2 -0
- package/dist/timeline/lane-timeline-types.d.ts +61 -0
- package/dist/timeline/lane-timeline-types.js +6 -0
- package/dist/timeline/lane-timeline.d.ts +2 -0
- package/dist/timeline/lane-timeline.js +85 -0
- package/dist/timeline/lane.d.ts +10 -0
- package/dist/timeline/lane.js +39 -0
- package/dist/timeline/public.d.ts +1 -0
- package/dist/timeline/time-axis.d.ts +18 -0
- package/dist/timeline/time-axis.js +22 -0
- package/package.json +24 -1
|
@@ -18,6 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
18
18
|
import { Maximize2, Minimize2, X } from 'lucide-react';
|
|
19
19
|
import { useEffect, useRef, useState } from 'react';
|
|
20
20
|
import { cn } from '../lib/utils.js';
|
|
21
|
+
import { useResolvedPanelViews } from './panel-views-provider.js';
|
|
21
22
|
import { useMedaShell } from './shell-provider.js';
|
|
22
23
|
import { useShellViewport } from './use-shell-viewport.js';
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -25,6 +26,7 @@ import { useShellViewport } from './use-shell-viewport.js';
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
const MIN_WIDTH = 300;
|
|
27
28
|
const MAX_WIDTH = 520;
|
|
29
|
+
const EMPTY_PANEL_VIEWS = [];
|
|
28
30
|
function ResizeHandle({ currentWidth, onResize, onCommit }) {
|
|
29
31
|
const startWidthRef = useRef(currentWidth);
|
|
30
32
|
const startXRef = useRef(0);
|
|
@@ -61,17 +63,28 @@ function ResizeHandle({ currentWidth, onResize, onCommit }) {
|
|
|
61
63
|
// ---------------------------------------------------------------------------
|
|
62
64
|
// RightPanel
|
|
63
65
|
// ---------------------------------------------------------------------------
|
|
64
|
-
export function RightPanel({ panelViews =
|
|
66
|
+
export function RightPanel({ panelViews = EMPTY_PANEL_VIEWS, defaultView, modes = ['panel', 'expanded', 'fullscreen'], className, }) {
|
|
65
67
|
const band = useShellViewport();
|
|
66
68
|
const ctx = useMedaShell();
|
|
67
69
|
const { mode, activeView, width, setMode, setActiveView, setWidth } = ctx.panel;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
const { panelViews: resolvedPanelViews, defaultView: resolvedDefaultView } = useResolvedPanelViews(panelViews, defaultView);
|
|
71
|
+
const hydratedDefaultRef = useRef(null);
|
|
72
|
+
// Hydrate a default when static or route-registered views become available.
|
|
70
73
|
useEffect(() => {
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const defaultExists = resolvedDefaultView != null &&
|
|
75
|
+
resolvedPanelViews.some((view) => view.id === resolvedDefaultView);
|
|
76
|
+
if (!defaultExists)
|
|
77
|
+
return;
|
|
78
|
+
const source = resolvedDefaultView !== defaultView ? 'registered' : 'static';
|
|
79
|
+
const canReplaceStaticDefault = activeView === hydratedDefaultRef.current?.id &&
|
|
80
|
+
hydratedDefaultRef.current.source === 'static' &&
|
|
81
|
+
source === 'registered' &&
|
|
82
|
+
activeView !== resolvedDefaultView;
|
|
83
|
+
if (!activeView || canReplaceStaticDefault) {
|
|
84
|
+
hydratedDefaultRef.current = { id: resolvedDefaultView, source };
|
|
85
|
+
setActiveView(resolvedDefaultView);
|
|
73
86
|
}
|
|
74
|
-
}, []);
|
|
87
|
+
}, [activeView, defaultView, resolvedDefaultView, resolvedPanelViews, setActiveView]);
|
|
75
88
|
// Local display width tracks pointer-move updates live;
|
|
76
89
|
// setWidth (persist) is called on pointerUp.
|
|
77
90
|
const [displayWidth, setDisplayWidth] = useState(null);
|
|
@@ -109,14 +122,14 @@ export function RightPanel({ panelViews = [], defaultView, modes = ['panel', 'ex
|
|
|
109
122
|
})();
|
|
110
123
|
// Z-index escalation: fullscreen covers header + rails
|
|
111
124
|
const zIndexClass = mode === 'fullscreen' ? 'z-[var(--z-shell-fullscreen)]' : 'z-[var(--z-shell-panel)]';
|
|
112
|
-
const activePanelView =
|
|
125
|
+
const activePanelView = resolvedPanelViews.find((v) => v.id === activeView);
|
|
113
126
|
const cycleAriaLabel = mode === 'panel' ? 'Expand panel' : mode === 'expanded' ? 'Maximize panel' : 'Restore panel';
|
|
114
127
|
const handleResize = (w) => setDisplayWidth(w);
|
|
115
128
|
const handleCommit = (w) => {
|
|
116
129
|
setDisplayWidth(null);
|
|
117
130
|
setWidth(w);
|
|
118
131
|
};
|
|
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:
|
|
132
|
+
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: resolvedPanelViews.map((view) => {
|
|
120
133
|
const isActive = view.id === activeView;
|
|
121
134
|
const Icon = view.icon;
|
|
122
135
|
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
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type ShellStorageAdapter } from './layout-state.js';
|
|
3
|
-
import type { AppDefinition, MobileBottomNavItem, PanelMode, ThemeAdapter, WorkspaceDefinition } from './types.js';
|
|
3
|
+
import type { AppDefinition, MobileBottomNavItem, PanelMode, PanelView, ThemeAdapter, WorkspaceDefinition } from './types.js';
|
|
4
4
|
export type MobileDrawerKind = 'menu-drawer' | 'module-drawer' | 'panels-drawer' | 'ai-drawer' | (string & {}) | null;
|
|
5
|
+
export interface PanelViewRegistration {
|
|
6
|
+
id: string;
|
|
7
|
+
views: PanelView[];
|
|
8
|
+
defaultView?: string;
|
|
9
|
+
}
|
|
5
10
|
interface MedaShellContextValue {
|
|
6
11
|
workspace: WorkspaceDefinition;
|
|
7
12
|
workspaces: WorkspaceDefinition[];
|
|
@@ -15,6 +20,9 @@ interface MedaShellContextValue {
|
|
|
15
20
|
setMode: (m: PanelMode) => void;
|
|
16
21
|
setActiveView: (v: string | null) => void;
|
|
17
22
|
setWidth: (w: number) => void;
|
|
23
|
+
open: () => void;
|
|
24
|
+
close: () => void;
|
|
25
|
+
toggle: () => void;
|
|
18
26
|
/** Opens panel + switches to viewId in one call.
|
|
19
27
|
* Sugar for app keyboard shortcuts (e.g. Cmd+J → panel.focus('ai')).
|
|
20
28
|
* If already open in 'panel', 'expanded', or 'fullscreen', the existing
|
|
@@ -26,6 +34,7 @@ interface MedaShellContextValue {
|
|
|
26
34
|
collapsed: boolean;
|
|
27
35
|
setWidth: (w: number) => void;
|
|
28
36
|
setCollapsed: (c: boolean) => void;
|
|
37
|
+
toggle: () => void;
|
|
29
38
|
};
|
|
30
39
|
mobileBottomNav: MobileBottomNavItem[];
|
|
31
40
|
mobileDrawer: {
|
|
@@ -36,6 +45,10 @@ interface MedaShellContextValue {
|
|
|
36
45
|
open: boolean;
|
|
37
46
|
setOpen: (open: boolean) => void;
|
|
38
47
|
};
|
|
48
|
+
panelViews: {
|
|
49
|
+
registrations: PanelViewRegistration[];
|
|
50
|
+
register: (id: string, views: PanelView[], defaultView?: string) => () => void;
|
|
51
|
+
};
|
|
39
52
|
commandPaletteHotkey: string;
|
|
40
53
|
/** Selection bridge between main workspace and right panel views (spec §17). */
|
|
41
54
|
selection: unknown | null;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { LayoutGrid, Menu, PanelTop, Sparkles } from 'lucide-react';
|
|
4
|
-
import { createContext, lazy, Suspense, useContext, useMemo, useState, } from 'react';
|
|
4
|
+
import { createContext, lazy, Suspense, useCallback, useContext, useMemo, useState, } from 'react';
|
|
5
5
|
import { createLocalStorageAdapter, useShellLayoutState, } from './layout-state.js';
|
|
6
6
|
import { DefaultThemeProvider, ThemeCtx } from './theme.js';
|
|
7
|
+
import { useShellViewport } from './use-shell-viewport.js';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Theme adapter wiring
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -78,11 +79,35 @@ export function MedaShellProvider(props) {
|
|
|
78
79
|
open: commandPaletteOpen,
|
|
79
80
|
setOpen: setCommandPaletteOpen,
|
|
80
81
|
}), [commandPaletteOpen]);
|
|
82
|
+
const [panelViewRegistrations, setPanelViewRegistrations] = useState([]);
|
|
83
|
+
const registerPanelViews = useCallback((id, views, defaultView) => {
|
|
84
|
+
setPanelViewRegistrations((prev) => {
|
|
85
|
+
const existing = prev.find((registration) => registration.id === id);
|
|
86
|
+
if (existing?.views === views && existing.defaultView === defaultView)
|
|
87
|
+
return prev;
|
|
88
|
+
const nextRegistration = defaultView === undefined ? { id, views } : { id, views, defaultView };
|
|
89
|
+
if (existing == null)
|
|
90
|
+
return [...prev, nextRegistration];
|
|
91
|
+
return prev.map((registration) => (registration.id === id ? nextRegistration : registration));
|
|
92
|
+
});
|
|
93
|
+
return () => {
|
|
94
|
+
setPanelViewRegistrations((prev) => prev.some((registration) => registration.id === id &&
|
|
95
|
+
registration.views === views &&
|
|
96
|
+
registration.defaultView === defaultView)
|
|
97
|
+
? prev.filter((registration) => registration.id !== id)
|
|
98
|
+
: prev);
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
const panelViews = useMemo(() => ({
|
|
102
|
+
registrations: panelViewRegistrations,
|
|
103
|
+
register: registerPanelViews,
|
|
104
|
+
}), [panelViewRegistrations, registerPanelViews]);
|
|
81
105
|
const [layoutState, setLayoutState] = useShellLayoutState({
|
|
82
106
|
workspaceId: props.workspace.id,
|
|
83
107
|
appId: activeAppId,
|
|
84
108
|
storage,
|
|
85
109
|
});
|
|
110
|
+
const isMobile = useShellViewport() === 'mobile';
|
|
86
111
|
const panel = useMemo(() => ({
|
|
87
112
|
mode: layoutState.rightPanel.mode,
|
|
88
113
|
activeView: layoutState.rightPanel.activeView,
|
|
@@ -99,6 +124,37 @@ export function MedaShellProvider(props) {
|
|
|
99
124
|
...prev,
|
|
100
125
|
rightPanel: { ...prev.rightPanel, width },
|
|
101
126
|
})),
|
|
127
|
+
open: () => {
|
|
128
|
+
if (isMobile)
|
|
129
|
+
setMobileDrawerOpen('panels-drawer');
|
|
130
|
+
setLayoutState((prev) => ({
|
|
131
|
+
...prev,
|
|
132
|
+
rightPanel: {
|
|
133
|
+
...prev.rightPanel,
|
|
134
|
+
mode: prev.rightPanel.mode === 'closed' ? 'panel' : prev.rightPanel.mode,
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
},
|
|
138
|
+
close: () => {
|
|
139
|
+
if (isMobile)
|
|
140
|
+
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : open));
|
|
141
|
+
setLayoutState((prev) => ({
|
|
142
|
+
...prev,
|
|
143
|
+
rightPanel: { ...prev.rightPanel, mode: 'closed' },
|
|
144
|
+
}));
|
|
145
|
+
},
|
|
146
|
+
toggle: () => {
|
|
147
|
+
if (isMobile) {
|
|
148
|
+
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : 'panels-drawer'));
|
|
149
|
+
}
|
|
150
|
+
setLayoutState((prev) => ({
|
|
151
|
+
...prev,
|
|
152
|
+
rightPanel: {
|
|
153
|
+
...prev.rightPanel,
|
|
154
|
+
mode: prev.rightPanel.mode === 'closed' ? 'panel' : 'closed',
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
},
|
|
102
158
|
// focus(viewId) — opens panel + switches to view in one call.
|
|
103
159
|
// Only flips closed → panel; preserves expanded / fullscreen modes.
|
|
104
160
|
focus: (viewId) => setLayoutState((prev) => {
|
|
@@ -108,7 +164,7 @@ export function MedaShellProvider(props) {
|
|
|
108
164
|
rightPanel: { ...prev.rightPanel, mode: nextMode, activeView: viewId },
|
|
109
165
|
};
|
|
110
166
|
}),
|
|
111
|
-
}), [layoutState, setLayoutState]);
|
|
167
|
+
}), [isMobile, layoutState, setLayoutState]);
|
|
112
168
|
const contextRail = useMemo(() => ({
|
|
113
169
|
width: layoutState.contextRail.width,
|
|
114
170
|
collapsed: layoutState.contextRail.collapsed,
|
|
@@ -120,6 +176,10 @@ export function MedaShellProvider(props) {
|
|
|
120
176
|
...prev,
|
|
121
177
|
contextRail: { ...prev.contextRail, collapsed },
|
|
122
178
|
})),
|
|
179
|
+
toggle: () => setLayoutState((prev) => ({
|
|
180
|
+
...prev,
|
|
181
|
+
contextRail: { ...prev.contextRail, collapsed: !prev.contextRail.collapsed },
|
|
182
|
+
})),
|
|
123
183
|
}), [layoutState, setLayoutState]);
|
|
124
184
|
const value = useMemo(() => ({
|
|
125
185
|
workspace: props.workspace,
|
|
@@ -132,6 +192,7 @@ export function MedaShellProvider(props) {
|
|
|
132
192
|
mobileBottomNav: props.mobileBottomNav ?? defaultMobileBottomNav,
|
|
133
193
|
mobileDrawer,
|
|
134
194
|
commandPalette,
|
|
195
|
+
panelViews,
|
|
135
196
|
commandPaletteHotkey: props.commandPaletteHotkey ?? 'mod+k',
|
|
136
197
|
selection,
|
|
137
198
|
setSelection,
|
|
@@ -145,6 +206,7 @@ export function MedaShellProvider(props) {
|
|
|
145
206
|
props.mobileBottomNav,
|
|
146
207
|
mobileDrawer,
|
|
147
208
|
commandPalette,
|
|
209
|
+
panelViews,
|
|
148
210
|
props.commandPaletteHotkey,
|
|
149
211
|
selection,
|
|
150
212
|
]);
|
package/dist/shell/types.d.ts
CHANGED
|
@@ -14,7 +14,8 @@ export interface ContextModule {
|
|
|
14
14
|
id: string;
|
|
15
15
|
label: string;
|
|
16
16
|
description?: string;
|
|
17
|
-
items
|
|
17
|
+
items?: ContextItem[];
|
|
18
|
+
render?: (ctx: ShellRenderContext) => ReactNode;
|
|
18
19
|
}
|
|
19
20
|
export interface ContextItem {
|
|
20
21
|
id: string;
|
|
@@ -55,6 +56,8 @@ export interface CommandDefinition {
|
|
|
55
56
|
icon?: LucideIcon;
|
|
56
57
|
group: string;
|
|
57
58
|
shortcut?: string;
|
|
59
|
+
/** Display-only alias for `shortcut`; `shortcut` wins when both are supplied. */
|
|
60
|
+
hotkey?: string;
|
|
58
61
|
run: () => void | Promise<void>;
|
|
59
62
|
}
|
|
60
63
|
export interface ThemeAdapter {
|
|
@@ -76,6 +79,7 @@ export interface AppShellIconRailConfig {
|
|
|
76
79
|
utilityItems?: import('./icon-rail.js').IconRailItem[];
|
|
77
80
|
footer?: ReactNode;
|
|
78
81
|
activeId?: string;
|
|
82
|
+
renderLink?: import('./icon-rail.js').IconRailProps['renderLink'];
|
|
79
83
|
}
|
|
80
84
|
/** ContextRail configuration for `<AppShell variant="workspace">`. */
|
|
81
85
|
export interface AppShellContextRailConfig {
|
package/dist/timeline/index.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ export type { DateSwitcherProps } from './date-switcher.js';
|
|
|
2
2
|
export { DateSwitcher } from './date-switcher.js';
|
|
3
3
|
export type { EventCardProps, EventCardSize } from './event-card.js';
|
|
4
4
|
export { EventCard } from './event-card.js';
|
|
5
|
+
export { LaneTimeline } from './lane-timeline.js';
|
|
6
|
+
export type { Lane, LaneBar, LaneLegendItem, LaneTimelineLabels, LaneTimelineProps, LaneTimelineRange, } from './lane-timeline-types.js';
|
|
7
|
+
export { defaultLaneTimelineLabels } from './lane-timeline-types.js';
|
|
5
8
|
export type { LiveIndicatorProps } from './live-indicator.js';
|
|
6
9
|
export { LiveIndicator } from './live-indicator.js';
|
|
7
10
|
export type { ScrubBarProps } from './scrub-bar.js';
|
package/dist/timeline/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { DateSwitcher } from './date-switcher.js';
|
|
2
2
|
export { EventCard } from './event-card.js';
|
|
3
|
+
export { LaneTimeline } from './lane-timeline.js';
|
|
4
|
+
export { defaultLaneTimelineLabels } from './lane-timeline-types.js';
|
|
3
5
|
export { LiveIndicator } from './live-indicator.js';
|
|
4
6
|
export { ScrubBar } from './scrub-bar.js';
|
|
5
7
|
export { TimelineRail } from './timeline-rail.js';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export type LaneTimelineRange = '1h' | '6h' | '24h' | '7d';
|
|
3
|
+
export interface LaneBar {
|
|
4
|
+
id: string;
|
|
5
|
+
start: Date;
|
|
6
|
+
end: Date;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Tailwind class for the bar fill, e.g. 'bg-success-500/80'. */
|
|
9
|
+
fillClass: string;
|
|
10
|
+
/** Optional left-edge accent class, e.g. 'border-l-4 border-l-success-700'. */
|
|
11
|
+
accentClass?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Lane {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
/** Optional secondary line under label (e.g. "3 sessions" or "offline"). */
|
|
17
|
+
sublabel?: string;
|
|
18
|
+
/** Tailwind class for the leading status dot (e.g. 'bg-success-500'). */
|
|
19
|
+
statusDotClass?: string;
|
|
20
|
+
bars: LaneBar[];
|
|
21
|
+
/** Render lane label muted; bars still render. */
|
|
22
|
+
muted?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface LaneLegendItem {
|
|
25
|
+
label: string;
|
|
26
|
+
swatchClass: string;
|
|
27
|
+
}
|
|
28
|
+
export interface LaneTimelineLabels {
|
|
29
|
+
previousDate: string;
|
|
30
|
+
nextDate: string;
|
|
31
|
+
now: string;
|
|
32
|
+
/** "{n} active" — `{n}` replaced at render. */
|
|
33
|
+
activeCount: string;
|
|
34
|
+
}
|
|
35
|
+
export declare const defaultLaneTimelineLabels: LaneTimelineLabels;
|
|
36
|
+
export interface TimeTick {
|
|
37
|
+
/** Display label, e.g. "09:00". */
|
|
38
|
+
label: string;
|
|
39
|
+
/** Position as percentage 0–100 along the time axis. */
|
|
40
|
+
position: number;
|
|
41
|
+
}
|
|
42
|
+
export interface LaneTimelineProps {
|
|
43
|
+
lanes: Lane[];
|
|
44
|
+
/** Reference time for now-line + window end. Defaults to `new Date()`. */
|
|
45
|
+
now?: Date;
|
|
46
|
+
defaultRange?: LaneTimelineRange;
|
|
47
|
+
range?: LaneTimelineRange;
|
|
48
|
+
onRangeChange?: (range: LaneTimelineRange) => void;
|
|
49
|
+
selectedDate?: Date;
|
|
50
|
+
onDateChange?: (date: Date) => void;
|
|
51
|
+
title?: ReactNode;
|
|
52
|
+
/** Render-prop slot for a "Group: …" chip in the header. */
|
|
53
|
+
groupChip?: ReactNode;
|
|
54
|
+
/** Number of currently-active bars; powers the "● {n} active" indicator. */
|
|
55
|
+
activeCount?: number;
|
|
56
|
+
legend?: LaneLegendItem[];
|
|
57
|
+
selectedBarId?: string;
|
|
58
|
+
onSelectBar?: (bar: LaneBar, lane: Lane) => void;
|
|
59
|
+
labels?: Partial<LaneTimelineLabels>;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { type LaneTimelineProps } from './lane-timeline-types.js';
|
|
2
|
+
export declare function LaneTimeline({ lanes, now, defaultRange, range: controlledRange, onRangeChange, selectedDate: controlledDate, onDateChange, title, groupChip, activeCount, legend, selectedBarId, onSelectBar, labels, className, }: LaneTimelineProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
import { LaneRow } from './lane.js';
|
|
6
|
+
import { defaultLaneTimelineLabels, } from './lane-timeline-types.js';
|
|
7
|
+
import { TimeAxis } from './time-axis.js';
|
|
8
|
+
const LABEL_GUTTER_PX = 140;
|
|
9
|
+
const RANGE_MS = {
|
|
10
|
+
'1h': 60 * 60_000,
|
|
11
|
+
'6h': 6 * 60 * 60_000,
|
|
12
|
+
'24h': 24 * 60 * 60_000,
|
|
13
|
+
'7d': 7 * 24 * 60 * 60_000,
|
|
14
|
+
};
|
|
15
|
+
export function LaneTimeline({ lanes, now, defaultRange = '6h', range: controlledRange, onRangeChange, selectedDate: controlledDate, onDateChange, title, groupChip, activeCount, legend, selectedBarId, onSelectBar, labels, className, }) {
|
|
16
|
+
const resolvedLabels = { ...defaultLaneTimelineLabels, ...(labels ?? {}) };
|
|
17
|
+
const [internalRange, setInternalRange] = useState(defaultRange);
|
|
18
|
+
const [internalDate, setInternalDate] = useState(() => now ?? new Date());
|
|
19
|
+
const range = controlledRange ?? internalRange;
|
|
20
|
+
const selectedDate = controlledDate ?? internalDate;
|
|
21
|
+
const referenceNow = now ?? new Date();
|
|
22
|
+
const { windowStart, windowEnd } = useMemo(() => {
|
|
23
|
+
const rangeMs = RANGE_MS[range];
|
|
24
|
+
const futurePadMs = Math.max(rangeMs / 6, 5 * 60_000);
|
|
25
|
+
const isToday = selectedDate.toDateString() === referenceNow.toDateString();
|
|
26
|
+
const end = isToday ? new Date(referenceNow.getTime() + futurePadMs) : endOfDay(selectedDate);
|
|
27
|
+
// futurePadMs is only relevant for the "today" path (gives the now-line breathing room).
|
|
28
|
+
// Historical days should render exactly rangeMs of window — no asymmetric pad.
|
|
29
|
+
const start = new Date(end.getTime() - rangeMs - (isToday ? futurePadMs : 0));
|
|
30
|
+
return { windowStart: start, windowEnd: end };
|
|
31
|
+
}, [range, selectedDate, referenceNow]);
|
|
32
|
+
const ticks = useMemo(() => buildTicks(windowStart, windowEnd, range), [windowStart, windowEnd, range]);
|
|
33
|
+
const handleRange = (r) => {
|
|
34
|
+
if (controlledRange === undefined)
|
|
35
|
+
setInternalRange(r);
|
|
36
|
+
onRangeChange?.(r);
|
|
37
|
+
};
|
|
38
|
+
const handleDate = (d) => {
|
|
39
|
+
if (controlledDate === undefined)
|
|
40
|
+
setInternalDate(d);
|
|
41
|
+
onDateChange?.(d);
|
|
42
|
+
};
|
|
43
|
+
const showNowLine = referenceNow.getTime() >= windowStart.getTime() &&
|
|
44
|
+
referenceNow.getTime() <= windowEnd.getTime();
|
|
45
|
+
const nowPositionPct = showNowLine
|
|
46
|
+
? ((referenceNow.getTime() - windowStart.getTime()) /
|
|
47
|
+
(windowEnd.getTime() - windowStart.getTime())) *
|
|
48
|
+
100
|
|
49
|
+
: 0;
|
|
50
|
+
return (_jsxs("section", { className: cn('flex flex-col rounded-lg border border-border bg-card p-4', className), children: [_jsx(TimeAxis, { range: range, onRangeChange: handleRange, selectedDate: selectedDate, onDateChange: handleDate, now: referenceNow, title: title, groupChip: groupChip, activeCount: activeCount, labels: resolvedLabels, ticks: ticks, labelGutterPx: LABEL_GUTTER_PX }), _jsxs("div", { className: "relative", children: [lanes.map((lane) => (_jsx(LaneRow, { lane: lane, windowStart: windowStart, windowEnd: windowEnd, labelGutterPx: LABEL_GUTTER_PX, selectedBarId: selectedBarId, onSelectBar: onSelectBar }, lane.id))), showNowLine && (_jsx("div", { "aria-hidden": "true", className: "pointer-events-none absolute top-0 bottom-0 w-px bg-primary", style: {
|
|
51
|
+
left: `calc(${LABEL_GUTTER_PX}px + (100% - ${LABEL_GUTTER_PX}px) * ${nowPositionPct / 100})`,
|
|
52
|
+
}, children: _jsx("div", { className: "-translate-x-1/2 -top-1 absolute size-2 rounded-full bg-primary" }) }))] }), legend && legend.length > 0 && (_jsxs("div", { className: "mt-3 flex items-center gap-4 border-border/40 border-t pt-3 text-muted-foreground text-xs", children: [_jsx("span", { className: "uppercase tracking-wider", children: "Legend" }), legend.map((item) => (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: cn('inline-block size-2 rounded-sm', item.swatchClass) }), item.label] }, item.label))), showNowLine && (_jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [_jsx("span", { className: "inline-block size-2 rounded-full bg-primary" }), resolvedLabels.now, " \u00B7 ", formatTime(referenceNow)] }))] }))] }));
|
|
53
|
+
}
|
|
54
|
+
function endOfDay(d) {
|
|
55
|
+
const e = new Date(d);
|
|
56
|
+
e.setHours(23, 59, 59, 999);
|
|
57
|
+
return e;
|
|
58
|
+
}
|
|
59
|
+
function formatTime(d) {
|
|
60
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
61
|
+
hour: '2-digit',
|
|
62
|
+
minute: '2-digit',
|
|
63
|
+
hour12: false,
|
|
64
|
+
}).format(d);
|
|
65
|
+
}
|
|
66
|
+
function buildTicks(start, end, range) {
|
|
67
|
+
const span = end.getTime() - start.getTime();
|
|
68
|
+
const tickCount = range === '7d' ? 7 : range === '24h' ? 8 : 7;
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = 0; i <= tickCount; i++) {
|
|
71
|
+
const t = new Date(start.getTime() + (span * i) / tickCount);
|
|
72
|
+
out.push({ position: (i / tickCount) * 100, label: formatTickLabel(t, range) });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
function formatTickLabel(d, range) {
|
|
77
|
+
if (range === '7d') {
|
|
78
|
+
return new Intl.DateTimeFormat(undefined, { weekday: 'short' }).format(d);
|
|
79
|
+
}
|
|
80
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
81
|
+
hour: '2-digit',
|
|
82
|
+
minute: '2-digit',
|
|
83
|
+
hour12: false,
|
|
84
|
+
}).format(d);
|
|
85
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Lane, LaneBar } from './lane-timeline-types.js';
|
|
2
|
+
export interface LaneRowProps {
|
|
3
|
+
lane: Lane;
|
|
4
|
+
windowStart: Date;
|
|
5
|
+
windowEnd: Date;
|
|
6
|
+
labelGutterPx: number;
|
|
7
|
+
selectedBarId?: string;
|
|
8
|
+
onSelectBar?: (bar: LaneBar, lane: Lane) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function LaneRow({ lane, windowStart, windowEnd, labelGutterPx, selectedBarId, onSelectBar, }: LaneRowProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
export function LaneRow({ lane, windowStart, windowEnd, labelGutterPx, selectedBarId, onSelectBar, }) {
|
|
5
|
+
return (_jsxs("div", { className: "grid items-center gap-0 border-border/40 border-b py-3", style: { gridTemplateColumns: `${labelGutterPx}px 1fr` }, children: [_jsxs("div", { className: "flex flex-col gap-0.5 pr-3", children: [_jsxs("div", { className: cn('flex items-center gap-2 text-sm', lane.muted && 'text-muted-foreground'), children: [lane.statusDotClass && (_jsx("span", { className: cn('inline-block size-2 rounded-full', lane.statusDotClass, lane.muted && 'opacity-40') })), _jsx("span", { className: "font-medium", children: lane.label })] }), lane.sublabel && _jsx("div", { className: "text-muted-foreground text-xs", children: lane.sublabel })] }), _jsx("div", { className: "relative h-7", children: lane.bars.map((bar) => {
|
|
6
|
+
const placement = placeBar(bar, windowStart, windowEnd);
|
|
7
|
+
if (!placement)
|
|
8
|
+
return null;
|
|
9
|
+
const selected = selectedBarId === bar.id;
|
|
10
|
+
const isSelectable = !!onSelectBar;
|
|
11
|
+
const barClassName = cn('absolute top-0 flex h-full items-center overflow-hidden rounded-md px-2 text-left text-xs text-white', bar.fillClass, bar.accentClass, isSelectable && [
|
|
12
|
+
'data-[selected]:ring-2 data-[selected]:ring-primary data-[selected]:ring-offset-1 data-[selected]:ring-offset-background',
|
|
13
|
+
'hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
14
|
+
]);
|
|
15
|
+
const barStyle = { left: `${placement.left}%`, width: `${placement.width}%` };
|
|
16
|
+
const ariaLabel = `${lane.label} — ${bar.label}`;
|
|
17
|
+
if (!isSelectable) {
|
|
18
|
+
return (_jsx("div", { role: "img", "aria-label": ariaLabel, className: barClassName, style: barStyle, children: _jsx("span", { "aria-hidden": "true", className: "truncate", children: bar.label }) }, bar.id));
|
|
19
|
+
}
|
|
20
|
+
return (_jsx("button", { type: "button", onClick: () => onSelectBar(bar, lane), "data-selected": selected || undefined, className: barClassName, style: barStyle, "aria-label": ariaLabel, children: _jsx("span", { "aria-hidden": "true", className: "truncate", children: bar.label }) }, bar.id));
|
|
21
|
+
}) })] }));
|
|
22
|
+
}
|
|
23
|
+
function placeBar(bar, windowStart, windowEnd) {
|
|
24
|
+
const ws = windowStart.getTime();
|
|
25
|
+
const we = windowEnd.getTime();
|
|
26
|
+
const span = we - ws;
|
|
27
|
+
if (span <= 0)
|
|
28
|
+
return null;
|
|
29
|
+
const bs = bar.start.getTime();
|
|
30
|
+
const be = bar.end.getTime();
|
|
31
|
+
// Bar entirely outside the window (or exactly touching boundary) — skip.
|
|
32
|
+
if (be <= ws || bs >= we)
|
|
33
|
+
return null;
|
|
34
|
+
const clampedStart = Math.max(bs, ws);
|
|
35
|
+
const clampedEnd = Math.min(be, we);
|
|
36
|
+
const left = ((clampedStart - ws) / span) * 100;
|
|
37
|
+
const width = Math.max(((clampedEnd - clampedStart) / span) * 100, 0.5);
|
|
38
|
+
return { left, width };
|
|
39
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { LaneTimelineLabels, LaneTimelineRange, TimeTick } from './lane-timeline-types.js';
|
|
3
|
+
export interface TimeAxisProps {
|
|
4
|
+
range: LaneTimelineRange;
|
|
5
|
+
onRangeChange: (range: LaneTimelineRange) => void;
|
|
6
|
+
selectedDate: Date;
|
|
7
|
+
onDateChange: (date: Date) => void;
|
|
8
|
+
/** Reference time used to determine whether `selectedDate` is "today" in the header label. */
|
|
9
|
+
now: Date;
|
|
10
|
+
title?: ReactNode;
|
|
11
|
+
groupChip?: ReactNode;
|
|
12
|
+
activeCount?: number;
|
|
13
|
+
labels: LaneTimelineLabels;
|
|
14
|
+
ticks: TimeTick[];
|
|
15
|
+
/** Width of the lane-label gutter on the left, in px (matches Lane component). */
|
|
16
|
+
labelGutterPx: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function TimeAxis({ range, onRangeChange, selectedDate, onDateChange, now, title, groupChip, activeCount, labels, ticks, labelGutterPx, }: TimeAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
const RANGES = ['1h', '6h', '24h', '7d'];
|
|
6
|
+
export function TimeAxis({ range, onRangeChange, selectedDate, onDateChange, now, title, groupChip, activeCount, labels, ticks, labelGutterPx, }) {
|
|
7
|
+
const stepDate = (delta) => {
|
|
8
|
+
const next = new Date(selectedDate);
|
|
9
|
+
next.setDate(next.getDate() + delta);
|
|
10
|
+
onDateChange(next);
|
|
11
|
+
};
|
|
12
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 border-border border-b pb-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [title && _jsx("div", { className: "font-medium text-sm", children: title }), _jsx("div", { className: "flex items-center gap-1 rounded-md bg-muted p-0.5", children: RANGES.map((r) => (_jsx("button", { type: "button", "aria-pressed": r === range, onClick: () => onRangeChange(r), "data-active": r === range || undefined, className: cn('rounded px-2 py-0.5 text-xs', 'data-[active]:bg-background data-[active]:text-foreground', r !== range && 'text-muted-foreground hover:text-foreground'), children: r }, r))) }), groupChip && _jsx("div", { className: "text-muted-foreground text-xs", children: groupChip }), typeof activeCount === 'number' && (_jsxs("div", { className: "text-muted-foreground text-xs", children: [_jsx("span", { className: "mr-1 inline-block size-1.5 rounded-full bg-success-500" }), labels.activeCount.replace('{n}', String(activeCount))] })), _jsxs("div", { className: "ml-auto flex items-center gap-1 rounded-md border border-border bg-card px-2 py-1 text-xs", children: [_jsx("button", { type: "button", "aria-label": labels.previousDate, onClick: () => stepDate(-1), className: "text-muted-foreground hover:text-foreground", children: _jsx(ChevronLeft, { className: "size-3.5" }) }), _jsx("div", { className: "px-1", children: formatDateHeader(selectedDate, now) }), _jsx("button", { type: "button", "aria-label": labels.nextDate, onClick: () => stepDate(1), className: "text-muted-foreground hover:text-foreground", children: _jsx(ChevronRight, { className: "size-3.5" }) })] })] }), _jsxs("div", { className: "grid items-end gap-0", style: { gridTemplateColumns: `${labelGutterPx}px 1fr` }, children: [_jsx("div", {}), _jsx("div", { className: "relative h-5", children: ticks.map((t) => (_jsx("div", { className: "-translate-x-1/2 absolute top-0 text-muted-foreground text-xs", style: { left: `${t.position}%` }, children: t.label }, `${t.position}:${t.label}`))) })] })] }));
|
|
13
|
+
}
|
|
14
|
+
function formatDateHeader(d, referenceNow) {
|
|
15
|
+
const isToday = d.toDateString() === referenceNow.toDateString();
|
|
16
|
+
const fmt = new Intl.DateTimeFormat(undefined, {
|
|
17
|
+
weekday: 'short',
|
|
18
|
+
month: 'short',
|
|
19
|
+
day: 'numeric',
|
|
20
|
+
}).format(d);
|
|
21
|
+
return isToday ? `Today · ${fmt}` : fmt;
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medalsocial/meda",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Shared Meda UI shell and runtime package.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
"types": "./dist/timeline/index.d.ts",
|
|
44
44
|
"default": "./dist/timeline/index.js"
|
|
45
45
|
},
|
|
46
|
+
"./kanban": {
|
|
47
|
+
"types": "./dist/kanban/index.d.ts",
|
|
48
|
+
"import": "./dist/kanban/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./list": {
|
|
51
|
+
"types": "./dist/list/index.d.ts",
|
|
52
|
+
"import": "./dist/list/index.js"
|
|
53
|
+
},
|
|
46
54
|
"./marketing": {
|
|
47
55
|
"types": "./dist/marketing/index.d.ts",
|
|
48
56
|
"default": "./dist/marketing/index.js"
|
|
@@ -73,6 +81,9 @@
|
|
|
73
81
|
"access": "public"
|
|
74
82
|
},
|
|
75
83
|
"peerDependencies": {
|
|
84
|
+
"@dnd-kit/core": "^6.3.1",
|
|
85
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
86
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
76
87
|
"@react-three/fiber": "^9.0.0",
|
|
77
88
|
"lucide-react": "^1.8.0",
|
|
78
89
|
"next-themes": "^0.4.0",
|
|
@@ -80,6 +91,15 @@
|
|
|
80
91
|
"react-dom": ">=19"
|
|
81
92
|
},
|
|
82
93
|
"peerDependenciesMeta": {
|
|
94
|
+
"@dnd-kit/core": {
|
|
95
|
+
"optional": false
|
|
96
|
+
},
|
|
97
|
+
"@dnd-kit/sortable": {
|
|
98
|
+
"optional": false
|
|
99
|
+
},
|
|
100
|
+
"@dnd-kit/utilities": {
|
|
101
|
+
"optional": false
|
|
102
|
+
},
|
|
83
103
|
"@react-three/fiber": {
|
|
84
104
|
"optional": true
|
|
85
105
|
},
|
|
@@ -103,6 +123,9 @@
|
|
|
103
123
|
"@biomejs/biome": "^2.4.12",
|
|
104
124
|
"@changesets/changelog-github": "^0.5.1",
|
|
105
125
|
"@changesets/cli": "^2.29.0",
|
|
126
|
+
"@dnd-kit/core": "^6.3.1",
|
|
127
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
128
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
106
129
|
"@react-three/drei": "^10.7.7",
|
|
107
130
|
"@react-three/fiber": "^9.6.0",
|
|
108
131
|
"@size-limit/preset-small-lib": "^12.1.0",
|