Skip to content

Commit b6c5c03

Browse files
authored
Merge pull request #460 from objectstack-ai/copilot/complete-roadmap-development-again
2 parents 328ad7a + a43707e commit b6c5c03

20 files changed

Lines changed: 1060 additions & 79 deletions

File tree

ROADMAP.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -221,21 +221,21 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
221221

222222
- [x] Implement DndConfigSchema-based DnD framework (unified API for Kanban, Dashboard, Calendar, Grid) — `DndProvider`, `useDnd` in @object-ui/react
223223
- [x] Consume DragItemSchema, DropZoneSchema, DragConstraintSchema, DropEffectSchema — types re-exported from @object-ui/types
224-
- [ ] Refactor plugin-kanban card drag to use spec DnD schemas
225-
- [ ] Refactor plugin-dashboard widget drag to use spec DnD schemas
226-
- [ ] Add drag-to-reschedule for calendar events
224+
- [x] Refactor plugin-kanban card drag to use spec DnD schemas — DndBridge bridges @dnd-kit events to ObjectUI DndProvider
225+
- [x] Refactor plugin-dashboard widget drag to use spec DnD schemas — DndEditModeBridge bridges edit mode to DndProvider
226+
- [x] Add drag-to-reschedule for calendar events — native HTML5 DnD in MonthView with `onEventDrop` callback
227227
- [ ] Add drag-and-drop sidebar navigation reordering
228228

229229
**Spec Reference:** `DndConfigSchema`, `DragItemSchema`, `DropZoneSchema`, `DragConstraintSchema`, `DragHandleSchema`, `DropEffectSchema`
230230

231231
#### 2.2 Gesture & Touch Support (2 weeks)
232232
**Target:** Mobile-first gesture handling aligned with spec schemas
233233

234-
- [ ] Integrate GestureConfigSchema and TouchInteractionSchema into @object-ui/mobile hooks
235-
- [ ] Consume SwipeGestureConfigSchema for navigation gestures
236-
- [ ] Consume PinchGestureConfigSchema for zoom interactions (maps, images)
237-
- [ ] Consume LongPressGestureConfigSchema for context menus
238-
- [ ] Consume TouchTargetConfigSchema for minimum touch target sizes (44px)
234+
- [x] Integrate GestureConfigSchema and TouchInteractionSchema into @object-ui/mobile hooks`useSpecGesture` hook
235+
- [x] Consume SwipeGestureConfigSchema for navigation gestures — integrated in `useSpecGesture`
236+
- [x] Consume PinchGestureConfigSchema for zoom interactions (maps, images) — integrated in `useSpecGesture`
237+
- [x] Consume LongPressGestureConfigSchema for context menus — integrated in `useSpecGesture`
238+
- [x] Consume TouchTargetConfigSchema for minimum touch target sizes (44px)`useTouchTarget` hook
239239

240240
**Spec Reference:** `GestureConfigSchema`, `SwipeGestureConfigSchema`, `PinchGestureConfigSchema`, `LongPressGestureConfigSchema`, `TouchInteractionSchema`, `TouchTargetConfigSchema`
241241

@@ -246,7 +246,7 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
246246
- [x] Implement FocusTrapConfigSchema for modal/drawer focus trapping — `useFocusTrap` with autoFocus, restoreFocus, escapeDeactivates
247247
- [x] Implement KeyboardNavigationConfigSchema for grid/list navigation (arrow keys, tab order) — `useKeyboardShortcuts` hook
248248
- [x] Implement KeyboardShortcutSchema system with help dialog (? key) — `useKeyboardShortcuts` + `getShortcutDescriptions` utility
249-
- [ ] Add keyboard shortcuts for common CRUD operations
249+
- [x] Add keyboard shortcuts for common CRUD operations`useCrudShortcuts` hook (Ctrl+N/E/S/D, Delete, Escape, Ctrl+F)
250250

251251
**Spec Reference:** `FocusManagementSchema`, `FocusTrapConfigSchema`, `KeyboardNavigationConfigSchema`, `KeyboardShortcutSchema`
252252

@@ -257,17 +257,17 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
257257
- [x] Implement MotionConfigSchema for reduced-motion preferences (`prefers-reduced-motion`) — `useReducedMotion` hook
258258
- [x] Implement TransitionConfigSchema and TransitionPresetSchema for view transitions — `useAnimation` with 7 presets (fade, slide-up/down/left/right, scale, scale-fade)
259259
- [x] Implement EasingFunctionSchema for consistent easing curves — easing presets (linear, ease, ease-in, ease-out, ease-in-out, spring)
260-
- [ ] Add animation to view switcher transitions
260+
- [x] Add animation to view switcher transitions — fade-in animation via Tailwind CSS `animate-in` classes
261261

262262
**Spec Reference:** `ComponentAnimationSchema`, `AnimationTriggerSchema`, `MotionConfigSchema`, `TransitionConfigSchema`, `TransitionPresetSchema`, `EasingFunctionSchema`
263263

264264
#### 2.5 View Enhancements (3 weeks)
265265
**Target:** Consume v2.0.7 view enhancement schemas in grid/list plugins
266266

267-
- [ ] Consume GalleryConfigSchema in plugin-list (gallery view layout, image sizing, masonry mode)
267+
- [x] Consume GalleryConfigSchema in plugin-list (gallery view layout, image sizing, masonry mode) — ObjectGallery with coverField/coverFit/cardSize/visibleFields
268268
- [x] Consume ColumnSummarySchema in plugin-grid and plugin-aggrid (column-level SUM/AVG/COUNT) — `useColumnSummary` hook
269-
- [ ] Consume GroupingConfigSchema and GroupingFieldSchema in plugin-grid (row grouping with subtotals)
270-
- [ ] Consume RowColorConfigSchema for conditional row coloring rules
269+
- [x] Consume GroupingConfigSchema and GroupingFieldSchema in plugin-grid (row grouping with subtotals)`useGroupedData` hook with collapsible sections
270+
- [x] Consume RowColorConfigSchema for conditional row coloring rules`useRowColor` hook with field-value color mapping
271271
- [x] Consume RowHeightSchema for compact/comfortable/spacious row height modes — `useDensityMode` hook
272272
- [x] Consume DensityMode for grid/list density toggling — `useDensityMode` with cycle()
273273
- [x] Consume ViewSharingSchema for shared/personal view configurations — `useViewSharing` hook with CRUD
@@ -281,7 +281,7 @@ The v2.0.7 spec introduces 70+ new UI types across 12 domains. This section maps
281281
- [x] Consume NotificationConfigSchema for position, duration, stacking — `NotificationSystemConfig` with all options
282282
- [x] Consume NotificationActionSchema for interactive notifications (buttons, links) — `NotificationActionButton` support
283283
- [x] Implement notification center UI with unread count badge — `useNotifications` with `unreadCount`, `markAsRead`, `markAllAsRead`
284-
- [ ] Integrate with `client.notifications.*` API for device registration and preferences
284+
- [x] Integrate with `client.notifications.*` API for device registration and preferences`useClientNotifications` hook
285285

286286
**Spec Reference:** `NotificationSchema`, `NotificationConfigSchema`, `NotificationActionSchema`, `NotificationPositionSchema`, `NotificationSeveritySchema`, `NotificationTypeSchema`
287287

packages/components/src/renderers/complex/data-table.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
100100
resizableColumns = true,
101101
reorderableColumns = true,
102102
editable = false,
103+
rowClassName,
103104
className,
104105
} = schema;
105106

@@ -665,7 +666,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
665666
data-state={isSelected ? 'selected' : undefined}
666667
className={cn(
667668
schema.onRowClick && "cursor-pointer",
668-
rowHasChanges && "bg-amber-50 dark:bg-amber-950/20"
669+
rowHasChanges && "bg-amber-50 dark:bg-amber-950/20",
670+
rowClassName && rowClassName(row, rowIndex)
669671
)}
670672
onClick={(e) => {
671673
if (schema.onRowClick && !e.defaultPrevented) {

packages/mobile/src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export { useBreakpoint, type BreakpointState } from './useBreakpoint';
2323
export { useResponsive } from './useResponsive';
2424
export { useResponsiveConfig, type SpecResponsiveConfig, type ResolvedResponsiveState } from './useResponsiveConfig';
2525
export { useGesture, type UseGestureOptions } from './useGesture';
26+
export { useSpecGesture, type UseSpecGestureOptions } from './useSpecGesture';
27+
export { useTouchTarget, type UseTouchTargetOptions, type TouchTargetResult } from './useTouchTarget';
2628
export { usePullToRefresh, type PullToRefreshOptions } from './usePullToRefresh';
2729
export { MobileProvider, type MobileProviderProps } from './MobileProvider';
2830
export { ResponsiveContainer, type ResponsiveContainerProps } from './ResponsiveContainer';
@@ -45,4 +47,10 @@ export type {
4547
GestureConfig,
4648
GestureContext,
4749
MobileComponentConfig,
50+
SpecGestureConfig,
51+
SwipeGestureConfig,
52+
PinchGestureConfig,
53+
LongPressGestureConfig,
54+
TouchInteraction,
55+
TouchTargetConfig,
4856
} from '@object-ui/types';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { useGesture } from './useGesture';
10+
import type { GestureType, SpecGestureConfig } from '@object-ui/types';
11+
12+
export interface UseSpecGestureOptions {
13+
/** Spec gesture configuration */
14+
config: SpecGestureConfig;
15+
/** Callback when a swipe gesture is detected */
16+
onSwipe?: (direction: string) => void;
17+
/** Callback when a pinch gesture is detected */
18+
onPinch?: (scale: number) => void;
19+
/** Callback when a long-press gesture is detected */
20+
onLongPress?: () => void;
21+
}
22+
23+
const SWIPE_DIRECTION_MAP: Record<string, GestureType> = {
24+
left: 'swipe-left',
25+
right: 'swipe-right',
26+
up: 'swipe-up',
27+
down: 'swipe-down',
28+
};
29+
30+
/**
31+
* Spec-aware gesture hook that maps an @objectstack/spec GestureConfig
32+
* to the existing useGesture hook.
33+
*
34+
* @example
35+
* ```tsx
36+
* const ref = useSpecGesture({
37+
* config: { type: 'swipe', enabled: true, swipe: { direction: 'left', threshold: 80 } },
38+
* onSwipe: (dir) => console.log('Swiped', dir),
39+
* });
40+
* return <div ref={ref}>Swipe me</div>;
41+
* ```
42+
*/
43+
export function useSpecGesture<T extends HTMLElement = HTMLElement>(
44+
options: UseSpecGestureOptions,
45+
) {
46+
const { config, onSwipe, onPinch, onLongPress } = options;
47+
const enabled = config.enabled ?? true;
48+
49+
let gestureType: GestureType = 'tap';
50+
let threshold: number | undefined;
51+
let longPressDuration: number | undefined;
52+
let onGesture: (ctx: { direction?: string; scale?: number }) => void = () => {};
53+
54+
if (config.swipe && onSwipe) {
55+
const dir = Array.isArray(config.swipe.direction)
56+
? config.swipe.direction[0]
57+
: config.swipe.direction;
58+
gestureType = (dir && SWIPE_DIRECTION_MAP[dir]) ?? 'swipe-left';
59+
threshold = config.swipe.threshold;
60+
onGesture = (ctx) => onSwipe(ctx.direction ?? dir ?? 'left');
61+
} else if (config.longPress && onLongPress) {
62+
gestureType = 'long-press';
63+
longPressDuration = config.longPress.duration;
64+
onGesture = () => onLongPress();
65+
} else if (config.pinch && onPinch) {
66+
gestureType = 'pinch';
67+
onGesture = (ctx) => onPinch(ctx.scale ?? 1);
68+
}
69+
70+
return useGesture<T>({
71+
type: gestureType,
72+
onGesture,
73+
threshold,
74+
longPressDuration,
75+
enabled,
76+
});
77+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import type { TouchTargetConfig } from '@object-ui/types';
10+
11+
export interface UseTouchTargetOptions {
12+
/** Spec touch target configuration */
13+
config?: TouchTargetConfig;
14+
}
15+
16+
export interface TouchTargetResult {
17+
/** CSS style properties enforcing minimum touch target sizes */
18+
style: {
19+
minWidth: string;
20+
minHeight: string;
21+
padding: string | undefined;
22+
};
23+
/** CSS class for touch-action optimization */
24+
className: string;
25+
}
26+
27+
/**
28+
* Hook that returns style and className props enforcing minimum
29+
* touch target sizes per the @objectstack/spec TouchTargetConfig.
30+
*
31+
* Defaults follow WCAG 2.5.5 (44×44 CSS pixels).
32+
*
33+
* @example
34+
* ```tsx
35+
* const target = useTouchTarget({ config: { minWidth: 48, minHeight: 48 } });
36+
* return <button style={target.style} className={target.className}>Tap</button>;
37+
* ```
38+
*/
39+
export function useTouchTarget(options: UseTouchTargetOptions = {}): TouchTargetResult {
40+
const { config } = options;
41+
const minWidth = config?.minWidth ?? 44;
42+
const minHeight = config?.minHeight ?? 44;
43+
const padding = config?.padding ?? 0;
44+
45+
return {
46+
style: {
47+
minWidth: `${minWidth}px`,
48+
minHeight: `${minHeight}px`,
49+
padding: padding > 0 ? `${padding}px` : undefined,
50+
},
51+
className: 'touch-manipulation',
52+
};
53+
}

packages/plugin-calendar/src/CalendarView.tsx

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface CalendarViewProps {
4646
onViewChange?: (view: "month" | "week" | "day") => void
4747
onNavigate?: (date: Date) => void
4848
onAddClick?: () => void
49+
onEventDrop?: (event: CalendarEvent, newStart: Date, newEnd?: Date) => void
4950
className?: string
5051
}
5152

@@ -59,6 +60,7 @@ function CalendarView({
5960
onViewChange,
6061
onNavigate,
6162
onAddClick,
63+
onEventDrop,
6264
className,
6365
}: CalendarViewProps) {
6466
const [selectedView, setSelectedView] = React.useState(view)
@@ -228,6 +230,7 @@ function CalendarView({
228230
events={events}
229231
onEventClick={onEventClick}
230232
onDateClick={onDateClick}
233+
onEventDrop={onEventDrop}
231234
/>
232235
)}
233236
{selectedView === "week" && (
@@ -322,12 +325,70 @@ interface MonthViewProps {
322325
events: CalendarEvent[]
323326
onEventClick?: (event: CalendarEvent) => void
324327
onDateClick?: (date: Date) => void
328+
onEventDrop?: (event: CalendarEvent, newStart: Date, newEnd?: Date) => void
325329
}
326330

327-
function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps) {
331+
function MonthView({ date, events, onEventClick, onDateClick, onEventDrop }: MonthViewProps) {
328332
const days = getMonthDays(date)
329333
const today = new Date()
330334
const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
335+
const [draggedEventId, setDraggedEventId] = React.useState<string | number | null>(null)
336+
const [dropTargetIndex, setDropTargetIndex] = React.useState<number | null>(null)
337+
338+
const handleDragStart = (e: React.DragEvent, event: CalendarEvent) => {
339+
setDraggedEventId(event.id)
340+
e.dataTransfer.effectAllowed = "move"
341+
e.dataTransfer.setData("text/plain", String(event.id))
342+
}
343+
344+
const handleDragEnd = () => {
345+
setDraggedEventId(null)
346+
setDropTargetIndex(null)
347+
}
348+
349+
const handleDragOver = (e: React.DragEvent, index: number) => {
350+
e.preventDefault()
351+
e.dataTransfer.dropEffect = "move"
352+
setDropTargetIndex(index)
353+
}
354+
355+
const handleDragLeave = (e: React.DragEvent) => {
356+
// Only clear when actually leaving the cell, not when moving over child elements
357+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
358+
setDropTargetIndex(null)
359+
}
360+
}
361+
362+
const handleDrop = (e: React.DragEvent, targetDay: Date) => {
363+
e.preventDefault()
364+
setDropTargetIndex(null)
365+
setDraggedEventId(null)
366+
367+
if (!onEventDrop) return
368+
369+
const eventId = e.dataTransfer.getData("text/plain")
370+
const draggedEvent = events.find((ev) => String(ev.id) === eventId)
371+
if (!draggedEvent) return
372+
373+
const oldStart = new Date(draggedEvent.start)
374+
const oldStartDay = new Date(oldStart)
375+
oldStartDay.setHours(0, 0, 0, 0)
376+
377+
const newTargetDay = new Date(targetDay)
378+
newTargetDay.setHours(0, 0, 0, 0)
379+
380+
const deltaMs = newTargetDay.getTime() - oldStartDay.getTime()
381+
if (deltaMs === 0) return
382+
383+
const newStart = new Date(oldStart.getTime() + deltaMs)
384+
385+
let newEnd: Date | undefined
386+
if (draggedEvent.end) {
387+
newEnd = new Date(new Date(draggedEvent.end).getTime() + deltaMs)
388+
}
389+
390+
onEventDrop(draggedEvent, newStart, newEnd)
391+
}
331392

332393
return (
333394
<div className="flex flex-col h-full">
@@ -355,9 +416,13 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
355416
key={index}
356417
className={cn(
357418
"border-b border-r last:border-r-0 p-2 min-h-[100px] cursor-pointer hover:bg-accent/50",
358-
!isCurrentMonth && "bg-muted/30 text-muted-foreground"
419+
!isCurrentMonth && "bg-muted/30 text-muted-foreground",
420+
dropTargetIndex === index && "ring-2 ring-primary"
359421
)}
360422
onClick={() => onDateClick?.(day)}
423+
onDragOver={(e) => handleDragOver(e, index)}
424+
onDragLeave={handleDragLeave}
425+
onDrop={(e) => handleDrop(e, day)}
361426
>
362427
<div
363428
className={cn(
@@ -372,9 +437,13 @@ function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps)
372437
{dayEvents.slice(0, 3).map((event) => (
373438
<div
374439
key={event.id}
440+
draggable={!!onEventDrop}
441+
onDragStart={(e) => handleDragStart(e, event)}
442+
onDragEnd={handleDragEnd}
375443
className={cn(
376444
"text-xs px-2 py-1 rounded truncate cursor-pointer hover:opacity-80",
377-
event.color || DEFAULT_EVENT_COLOR
445+
event.color || DEFAULT_EVENT_COLOR,
446+
draggedEventId === event.id && "opacity-50"
378447
)}
379448
style={
380449
event.color && event.color.startsWith("#")

0 commit comments

Comments
 (0)