@medalsocial/meda 1.1.1 → 1.2.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/drag-mode-banner.d.ts +31 -0
- package/dist/shell/drag-mode-banner.js +19 -0
- package/dist/shell/index.d.ts +6 -0
- package/dist/shell/index.js +4 -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/shell-main.js +1 -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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Checkbox } from '../components/ui/checkbox.js';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* A Linear-style list row with checkbox, hover states, and click handling.
|
|
7
|
+
* Compact ~40-48px height for dense, scannable lists.
|
|
8
|
+
*/
|
|
9
|
+
export function ListRow({ selected, selectionActive, onSelect, onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, focused, children, className, }) {
|
|
10
|
+
const isInteractive = !!onClick;
|
|
11
|
+
const interactiveProps = isInteractive
|
|
12
|
+
? {
|
|
13
|
+
role: 'button',
|
|
14
|
+
tabIndex: 0,
|
|
15
|
+
onClick,
|
|
16
|
+
onKeyDown: (e) => {
|
|
17
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
onClick?.();
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
: {};
|
|
24
|
+
return (_jsxs("div", { ...interactiveProps, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onFocus: onFocus, onBlur: onBlur, className: cn('group relative flex items-center gap-3 border-border border-b px-3 py-2.5', 'transition-all duration-150', isInteractive && 'cursor-pointer hover:bg-accent/50', isInteractive &&
|
|
25
|
+
"before:absolute before:inset-y-0 before:left-0 before:w-[2px] before:scale-y-0 before:bg-primary before:transition-transform before:duration-150 before:content-['']", isInteractive && 'hover:before:scale-y-100', isInteractive && 'focus-within:bg-accent/30 focus-within:before:scale-y-100', selected && isInteractive && 'before:!scale-y-100 bg-accent', selected && !isInteractive && 'bg-accent', focused &&
|
|
26
|
+
isInteractive &&
|
|
27
|
+
'before:!scale-y-100 bg-accent/30 ring-1 ring-primary/50 ring-inset', focused && !isInteractive && 'bg-accent/30 ring-1 ring-primary/50 ring-inset', className), children: [onSelect && (_jsx("div", { className: "w-6 flex-shrink-0", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: selected, onClick: (e) => {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
onSelect(!selected, e.shiftKey);
|
|
30
|
+
}, className: cn('transition-opacity', selectionActive || selected
|
|
31
|
+
? 'opacity-100'
|
|
32
|
+
: 'opacity-0 group-hover:opacity-100 data-[state=checked]:opacity-100') }) })), children] }));
|
|
33
|
+
}
|
|
34
|
+
export function ListCell({ width = 'flex-1', shrink = true, align = 'left', children, className, }) {
|
|
35
|
+
return (_jsx("div", { className: cn(width, shrink === false && 'flex-shrink-0', align === 'center' && 'text-center', align === 'right' && 'text-right', 'min-w-0', // Allow truncation
|
|
36
|
+
className), children: children }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Props for DragModeBanner.
|
|
4
|
+
*
|
|
5
|
+
* **Important:** This component uses `useDndMonitor` internally and must be
|
|
6
|
+
* rendered inside the same `<DndContext>` tree it is monitoring.
|
|
7
|
+
*
|
|
8
|
+
* **Known limitation:** The ESC chip is decorative only. Keyboard-initiated
|
|
9
|
+
* drags cancel naturally via dnd-kit's KeyboardSensor; pointer-initiated drags
|
|
10
|
+
* require the user to release outside any drop zone — a manual ESC handler for
|
|
11
|
+
* pointer drags is not yet implemented.
|
|
12
|
+
*/
|
|
13
|
+
export interface DragModeBannerProps {
|
|
14
|
+
/** Banner message shown while a drag is active. */
|
|
15
|
+
message: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* When provided, render an ESC chip indicating the user can press Escape to
|
|
18
|
+
* cancel. See known limitation note above.
|
|
19
|
+
*/
|
|
20
|
+
cancelKey?: 'ESC';
|
|
21
|
+
/**
|
|
22
|
+
* Optional predicate; when supplied only activates the banner for matching
|
|
23
|
+
* drag ids. Receives the `active.id` of the current drag.
|
|
24
|
+
*/
|
|
25
|
+
isActive?: (activeId: string | number) => boolean;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function DragModeBanner({ message, cancelKey, isActive, className }: DragModeBannerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
29
|
+
export declare namespace DragModeBanner {
|
|
30
|
+
var displayName: string;
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useDndMonitor } from '@dnd-kit/core';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
export function DragModeBanner({ message, cancelKey, isActive, className }) {
|
|
7
|
+
const [activeId, setActiveId] = useState(null);
|
|
8
|
+
useDndMonitor({
|
|
9
|
+
onDragStart: (e) => setActiveId(e.active.id),
|
|
10
|
+
onDragEnd: () => setActiveId(null),
|
|
11
|
+
onDragCancel: () => setActiveId(null),
|
|
12
|
+
});
|
|
13
|
+
if (activeId == null)
|
|
14
|
+
return null;
|
|
15
|
+
if (isActive && !isActive(activeId))
|
|
16
|
+
return null;
|
|
17
|
+
return (_jsxs("div", { role: "status", "aria-live": "polite", className: cn('pointer-events-none flex items-center justify-center gap-3 px-4 py-2 text-sm text-muted-foreground', className), children: [_jsx("span", { children: message }), cancelKey === 'ESC' && (_jsx("kbd", { className: "rounded border border-border bg-muted px-1.5 py-0.5 font-medium font-mono text-xs", children: "ESC" }))] }));
|
|
18
|
+
}
|
|
19
|
+
DragModeBanner.displayName = 'DragModeBanner';
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -2,11 +2,17 @@ export { AppShell, AppShellBody } from './app-shell.js';
|
|
|
2
2
|
export type { CommandGroupDefinition } from './command-palette.js';
|
|
3
3
|
export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
|
|
4
4
|
export { ContextRail } from './context-rail.js';
|
|
5
|
+
export type { DragModeBannerProps } from './drag-mode-banner.js';
|
|
6
|
+
export { DragModeBanner } from './drag-mode-banner.js';
|
|
5
7
|
export * as Extras from './extras/index.js';
|
|
6
8
|
export { IconRail, RailDivider } from './icon-rail.js';
|
|
7
9
|
export type { ShellStorageAdapter } from './layout-state.js';
|
|
8
10
|
export { createLocalStorageAdapter } from './layout-state.js';
|
|
9
11
|
export { motion } from './motion.js';
|
|
12
|
+
export type { RailDropSlotProps, RailDropSlotState } from './rail-drop-slot.js';
|
|
13
|
+
export { RailDropSlot } from './rail-drop-slot.js';
|
|
14
|
+
export type { RailDropZonesProps } from './rail-drop-zones.js';
|
|
15
|
+
export { RailDropZones } from './rail-drop-zones.js';
|
|
10
16
|
export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
|
|
11
17
|
export { RightPanel } from './right-panel.js';
|
|
12
18
|
export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-header.js';
|
package/dist/shell/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export { AppShell, AppShellBody } from './app-shell.js';
|
|
|
8
8
|
// Command palette
|
|
9
9
|
export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
|
|
10
10
|
export { ContextRail } from './context-rail.js';
|
|
11
|
+
// Drag-and-drop utilities
|
|
12
|
+
export { DragModeBanner } from './drag-mode-banner.js';
|
|
11
13
|
// Extras (legacy components ported during Phase 15 — opt-in for apps that need them)
|
|
12
14
|
export * as Extras from './extras/index.js';
|
|
13
15
|
// Rails + main + panel
|
|
@@ -17,6 +19,8 @@ export { createLocalStorageAdapter } from './layout-state.js';
|
|
|
17
19
|
// Hooks + tokens
|
|
18
20
|
export { motion } from './motion.js';
|
|
19
21
|
// Resize primitives
|
|
22
|
+
export { RailDropSlot } from './rail-drop-slot.js';
|
|
23
|
+
export { RailDropZones } from './rail-drop-zones.js';
|
|
20
24
|
export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
|
|
21
25
|
export { RightPanel } from './right-panel.js';
|
|
22
26
|
// Header (and its individual children for advanced composition)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Describes the slot's current relation to an active drag operation.
|
|
4
|
+
*
|
|
5
|
+
* - `'idle'` — no drag in progress.
|
|
6
|
+
* - `'active'` — a drag is in progress and this slot would accept it, but
|
|
7
|
+
* the cursor is not currently over this slot.
|
|
8
|
+
* - `'over'` — the dragged item is hovering over this slot and it would
|
|
9
|
+
* accept the drop.
|
|
10
|
+
* - `'rejected'` — the dragged item is hovering over this slot but the slot
|
|
11
|
+
* cannot accept it (disabled or `accepts` returns false), OR
|
|
12
|
+
* the slot is `disabled` while any drag is active.
|
|
13
|
+
*/
|
|
14
|
+
export type RailDropSlotState = 'idle' | 'active' | 'over' | 'rejected';
|
|
15
|
+
export interface RailDropSlotProps {
|
|
16
|
+
/** Stable id used by dnd-kit; also rendered as `data-slot-id`. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Decide whether this slot accepts the dragged card. */
|
|
19
|
+
accepts?: (activeId: string) => boolean;
|
|
20
|
+
/** Whether this slot can currently receive a drop (e.g. capacity, online). */
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Static content. Mutually exclusive with `render`.
|
|
24
|
+
* Use `render` when you need state-aware content.
|
|
25
|
+
*/
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* State-aware content factory. Receives the slot's current `RailDropSlotState`
|
|
29
|
+
* so you can render different UI for idle / active / over / rejected states.
|
|
30
|
+
* Mutually exclusive with `children`.
|
|
31
|
+
*/
|
|
32
|
+
render?: (state: RailDropSlotState) => ReactNode;
|
|
33
|
+
className?: string;
|
|
34
|
+
/** Visible label for assistive tech. */
|
|
35
|
+
ariaLabel?: string;
|
|
36
|
+
ref?: Ref<HTMLElement>;
|
|
37
|
+
}
|
|
38
|
+
export declare function RailDropSlot({ id, accepts, disabled, children, render, className, ariaLabel, ref, }: RailDropSlotProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export declare namespace RailDropSlot {
|
|
40
|
+
var displayName: string;
|
|
41
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useDndMonitor, useDroppable } from '@dnd-kit/core';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
export function RailDropSlot({ id, accepts, disabled, children, render, className, ariaLabel, ref, }) {
|
|
7
|
+
const { isOver, active, setNodeRef } = useDroppable({
|
|
8
|
+
id,
|
|
9
|
+
disabled,
|
|
10
|
+
data: { type: 'rail-drop-slot' },
|
|
11
|
+
});
|
|
12
|
+
const [globalDragActive, setGlobalDragActive] = useState(false);
|
|
13
|
+
useDndMonitor({
|
|
14
|
+
onDragStart: () => setGlobalDragActive(true),
|
|
15
|
+
onDragEnd: () => setGlobalDragActive(false),
|
|
16
|
+
onDragCancel: () => setGlobalDragActive(false),
|
|
17
|
+
});
|
|
18
|
+
const wouldAccept = !disabled && (!accepts || (active?.id != null && accepts(String(active.id))));
|
|
19
|
+
let state;
|
|
20
|
+
if (isOver && wouldAccept) {
|
|
21
|
+
state = 'over';
|
|
22
|
+
}
|
|
23
|
+
else if (isOver && !wouldAccept) {
|
|
24
|
+
state = 'rejected';
|
|
25
|
+
}
|
|
26
|
+
else if (globalDragActive && disabled) {
|
|
27
|
+
state = 'rejected';
|
|
28
|
+
}
|
|
29
|
+
else if (globalDragActive && wouldAccept) {
|
|
30
|
+
state = 'active';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
state = 'idle';
|
|
34
|
+
}
|
|
35
|
+
// Legacy boolean aliases kept for backward-compat data attributes
|
|
36
|
+
const showHover = state === 'over';
|
|
37
|
+
const showRejected = state === 'rejected';
|
|
38
|
+
return (_jsx("section", { ref: (el) => {
|
|
39
|
+
setNodeRef(el);
|
|
40
|
+
if (typeof ref === 'function')
|
|
41
|
+
ref(el);
|
|
42
|
+
else if (ref)
|
|
43
|
+
ref.current = el;
|
|
44
|
+
}, "data-slot-id": id, "data-state": state, "aria-label": ariaLabel, "aria-disabled": disabled || undefined, className: cn('relative rounded-lg border border-border bg-card transition-all duration-150', state === 'idle' && '', state === 'active' && 'border-dashed border-primary/60 bg-primary/5', state === 'over' &&
|
|
45
|
+
'border-primary bg-primary/5 ring-2 ring-primary/30 shadow-[0_0_0_4px_hsl(var(--primary)/0.1)]', state === 'rejected' && 'border-destructive/40 opacity-60',
|
|
46
|
+
// Legacy classes — keep working for consumers using data-state selectors
|
|
47
|
+
showHover && '', showRejected && '', disabled && state === 'idle' && 'opacity-60', className), children: render ? render(state) : children }));
|
|
48
|
+
}
|
|
49
|
+
RailDropSlot.displayName = 'RailDropSlot';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface RailDropZonesProps {
|
|
3
|
+
/** Section title — uppercased in the header. Defaults to "Drop zones". */
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Subtitle shown under the title (e.g. "Eligible machines for ENG-405 · claude"). */
|
|
6
|
+
subtitle?: ReactNode;
|
|
7
|
+
/** Count badge rendered in the header corner, e.g. "3 / 4". */
|
|
8
|
+
count?: ReactNode;
|
|
9
|
+
/** Optional icon rendered at the start of the header. */
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* When set, overrides the auto-detected drag-active state. Useful for
|
|
13
|
+
* Storybook stories that want to demonstrate the active visual without a
|
|
14
|
+
* real drag in progress.
|
|
15
|
+
*/
|
|
16
|
+
forceActive?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Optional predicate; when supplied only activates the rail outline for
|
|
19
|
+
* matching drag ids. Receives `active.id` of the current drag.
|
|
20
|
+
*/
|
|
21
|
+
isActive?: (activeId: string | number) => boolean;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function RailDropZones({ title, subtitle, count, icon, forceActive, isActive, children, className, }: RailDropZonesProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare namespace RailDropZones {
|
|
27
|
+
var displayName: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useDndMonitor } from '@dnd-kit/core';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { cn } from '../lib/utils.js';
|
|
6
|
+
export function RailDropZones({ title = 'Drop zones', subtitle, count, icon, forceActive, isActive, children, className, }) {
|
|
7
|
+
const [activeId, setActiveId] = useState(null);
|
|
8
|
+
useDndMonitor({
|
|
9
|
+
onDragStart: (e) => setActiveId(e.active.id),
|
|
10
|
+
onDragEnd: () => setActiveId(null),
|
|
11
|
+
onDragCancel: () => setActiveId(null),
|
|
12
|
+
});
|
|
13
|
+
const auto = activeId != null && (!isActive || isActive(activeId));
|
|
14
|
+
const active = forceActive ?? auto;
|
|
15
|
+
return (_jsxs("section", { "data-active": active || undefined, className: cn('flex flex-col gap-3 rounded-lg border border-transparent p-3 transition-colors', active && 'border-2 border-dashed border-primary/60 bg-primary/5', className), children: [(title || subtitle || count) && (_jsxs("header", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-start gap-2", children: [icon && _jsx("span", { className: "text-primary", children: icon }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [title && (_jsx("span", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wider", children: title })), subtitle && _jsx("span", { className: "text-muted-foreground text-xs", children: subtitle })] })] }), count && _jsx("span", { className: "text-muted-foreground text-xs", children: count })] })), _jsx("div", { className: "flex flex-col gap-2", children: children })] }));
|
|
16
|
+
}
|
|
17
|
+
RailDropZones.displayName = 'RailDropZones';
|
package/dist/shell/shell-main.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from '../lib/utils.js';
|
|
4
4
|
const layoutClass = {
|
|
5
|
-
workspace: '
|
|
5
|
+
workspace: 'w-full max-w-[1280px] px-4 py-6 sm:px-6 lg:px-8',
|
|
6
6
|
centered: 'mx-auto w-full max-w-2xl px-4 py-6 sm:px-6',
|
|
7
7
|
fullbleed: 'w-full',
|
|
8
8
|
};
|
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.2.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",
|