Skip to content

feat: implement Q2 2026 roadmap — spec type re-exports, focus/keyboard, animation, notifications, DnD, view enhancements#459

Merged
hotlong merged 9 commits intomainfrom
copilot/update-development-progress
Feb 11, 2026
Merged

feat: implement Q2 2026 roadmap — spec type re-exports, focus/keyboard, animation, notifications, DnD, view enhancements#459
hotlong merged 9 commits intomainfrom
copilot/update-development-progress

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 11, 2026

Advances spec v2.0.7 compliance from 86% → 90% by implementing the Q2 2026 interactive experience layer: focus management, animation/motion, notifications, DnD coordination, and view enhancement hooks.

Spec type re-exports (@object-ui/types)

  • Re-export all 70+ new v2.0.7 UI types from @objectstack/spec/ui, organized into 11 domain sections (DnD, Focus, Animation, Notifications, Gestures, Offline/Sync, View Enhancements, Performance, Page Transitions, Accessibility, I18n, Responsive)
  • Alias colliding names with Spec prefix (SpecGestureConfig, SpecOfflineConfig, SpecNotification, etc.)

New hooks (@object-ui/react)

  • useFocusTrap — Tab/Shift+Tab cycling, auto-focus, restore-focus, escape-deactivate
  • useKeyboardShortcuts — Modifier key support (Ctrl/Cmd/Shift/Alt), form element awareness, getShortcutDescriptions() for help dialogs
  • useAnimation — 7 transition presets mapped to Tailwind classes (fade, slide-*, scale, scale-fade) with custom duration/delay/easing
  • useReducedMotion — Live prefers-reduced-motion media query listener
  • useColumnSummary — SUM/AVG/COUNT/MIN/MAX/DISTINCT with Intl.NumberFormat
  • useDensityMode — Compact/comfortable/spacious with row heights, padding/font classes, cycle()
  • useViewSharing — Saved view CRUD with private/team/org/public visibility

New context providers (@object-ui/react)

  • NotificationProvider / useNotifications — Severity levels, auto-dismiss, unread tracking, external toast delegation via onToast
  • DndProvider / useDnd — Drop zone registration, accept-type filtering, constraint config, effect types (move/copy/link)
// Focus trap + keyboard shortcuts
const trapRef = useFocusTrap({ autoFocus: true, escapeDeactivates: true, onEscape: close });
useKeyboardShortcuts([
  { key: 'k', ctrlOrMeta: true, handler: openSearch, description: 'Search' },
]);

// Animation with reduced motion respect
const prefersReduced = useReducedMotion();
const anim = useAnimation({ preset: prefersReduced ? 'none' : 'fade', duration: 200 });

// View density + column summary
const density = useDensityMode('comfortable');
const total = useColumnSummary(rows, { field: 'amount', function: 'sum', label: 'Total' });

Tests

8 new test files, 125 new tests. All 256 react package tests pass, all 41 monorepo builds pass.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Canceled Canceled Feb 11, 2026 8:52pm
objectui-demo Canceled Canceled Feb 11, 2026 8:52pm
objectui-storybook Error Error Feb 11, 2026 8:52pm

Request Review

Copilot AI and others added 8 commits February 11, 2026 17:23
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…s management & keyboard navigation

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ion System

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…seViewSharing)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…uts, useAnimation, useColumnSummary, useDensityMode, useViewSharing, NotificationContext, DndContext)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…6% → 90% compliance)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Update roadmap with development progress feat: implement Q2 2026 roadmap — spec type re-exports, focus/keyboard, animation, notifications, DnD, view enhancements Feb 11, 2026
Copilot AI requested a review from hotlong February 11, 2026 17:51
@hotlong hotlong marked this pull request as ready for review February 11, 2026 17:59
Copilot AI review requested due to automatic review settings February 11, 2026 17:59
@hotlong hotlong merged commit 328ad7a into main Feb 11, 2026
3 of 6 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances ObjectUI’s v2.0.7 compliance by expanding spec type re-exports in @object-ui/types and adding a new interactive “experience layer” in @object-ui/react (hooks + context providers) for focus/keyboard, motion/animation, notifications, DnD coordination, and view enhancements.

Changes:

  • Re-export v2.0.7 spec UI types from @objectstack/spec/ui via @object-ui/types, with Spec* aliases for some colliding names.
  • Add new React hooks (useFocusTrap, useKeyboardShortcuts, useReducedMotion, useAnimation, useColumnSummary, useDensityMode, useViewSharing) and corresponding test coverage.
  • Introduce NotificationProvider/useNotifications and DndProvider/useDnd contexts (plus tests) and update the roadmap metrics.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/types/src/index.ts Adds v2.0.7 UI type re-exports from @objectstack/spec/ui organized by domain.
packages/react/src/hooks/useViewSharing.ts Adds a saved-view CRUD hook for view sharing/visibility.
packages/react/src/hooks/useReducedMotion.ts Adds a prefers-reduced-motion listener hook.
packages/react/src/hooks/useKeyboardShortcuts.ts Adds a global keyboard shortcut registration hook + description helper.
packages/react/src/hooks/useFocusTrap.ts Adds a focus-trap hook for modal/drawer-style containment.
packages/react/src/hooks/useDensityMode.ts Adds a compact/comfortable/spacious density mode hook.
packages/react/src/hooks/useColumnSummary.ts Adds a column aggregation hook (sum/avg/count/min/max/distinct) with formatting.
packages/react/src/hooks/useAnimation.ts Adds preset-based animation class generation with configurable timing.
packages/react/src/hooks/index.ts Exports the newly added hooks from the hooks barrel file.
packages/react/src/hooks/tests/*.test.ts Adds unit tests for the new hooks.
packages/react/src/context/index.ts Exports the new Notification and DnD contexts.
packages/react/src/context/NotificationContext.tsx Adds NotificationProvider and related notification APIs.
packages/react/src/context/DndContext.tsx Adds DndProvider coordination layer for DnD state and drop-zone registration.
packages/react/src/context/tests/*.test.tsx Adds unit tests for the new contexts.
ROADMAP.md Updates roadmap checkboxes and compliance percentages to reflect these features.

Comment on lines +156 to +158
const [activeItem, setActiveItem] = useState<DragItemData | null>(null);
const [dropZones] = useState<Map<string, DropZoneConfig>>(() => new Map());

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropZones is a Map that gets mutated in-place. Since the Map reference never changes, context consumers won’t re-render when drop zones are registered/unregistered. Consider clone-on-write with a state setter (replace the Map) or store the Map in a ref plus a version state to trigger updates.

Copilot uses AI. Check for mistakes.
views: SavedView[];
/** Currently active view */
activeView: SavedView | null;
/** Save a new view or update existing */
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The saveView JSDoc/API says it can "save a new view or update existing", but the current signature (Omit<..., 'id' | 'updatedAt'>) and implementation always create a new id and append a new view. Either add update/upsert support (e.g., accept an id) or rename this to reflect create-only behavior.

Suggested change
/** Save a new view or update existing */
/** Save a new view */

Copilot uses AI. Check for mistakes.
// v2.0.7 Spec UI Types — Notifications
// ============================================================================
export type {
Notification,
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notification is re-exported unaliased from @objectstack/spec/ui. Since Notification is also a global DOM type, importing this can create confusing name collisions in consumer code. Consider exporting it as SpecNotification (similar to the other Spec* aliases here).

Suggested change
Notification,
Notification as SpecNotification,

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +60
const target = event.target as HTMLElement;
const isFormElement =
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable;

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event.target can be null or a non-HTMLElement (e.g. document), but this code casts to HTMLElement and reads tagName, which can throw. Guard with event.target instanceof HTMLElement (and handle null) before accessing element properties.

Suggested change
const target = event.target as HTMLElement;
const isFormElement =
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable;
const target = event.target;
let isFormElement = false;
if (target instanceof HTMLElement) {
const tagName = target.tagName;
isFormElement =
tagName === 'INPUT' ||
tagName === 'TEXTAREA' ||
tagName === 'SELECT' ||
target.isContentEditable;
}

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +163
// Auto-dismiss non-persistent notifications
const duration = input.duration ?? config.defaultDuration ?? 5000;
if (duration > 0) {
setTimeout(() => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-dismiss uses setTimeout, but the timer isn’t tracked/cleared. If the provider unmounts before the timeout fires, it will call setNotifications on an unmounted component. Track timeout ids (e.g., in a ref keyed by notification id) and clear them on unmount / early dismiss.

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +188
const registerDropZone = useCallback(
(zone: DropZoneConfig) => {
dropZones.set(zone.id, zone);
},
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerDropZone mutates the dropZones map via dropZones.set(...) without triggering any state update. To keep consumers reactive, update this to replace the map (e.g., setDropZones(new Map(prev).set(...))) or bump a separate version state.

Copilot uses AI. Check for mistakes.
Comment on lines +187 to +196
dropZones.set(zone.id, zone);
},
[dropZones],
);

const unregisterDropZone = useCallback(
(zoneId: string) => {
dropZones.delete(zoneId);
},
[dropZones],
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, unregisterDropZone mutates the dropZones map via dropZones.delete(...) without a state update, so dependent UI won’t re-render. Consider using a setter that replaces the map (clone-on-write) when deleting.

Suggested change
dropZones.set(zone.id, zone);
},
[dropZones],
);
const unregisterDropZone = useCallback(
(zoneId: string) => {
dropZones.delete(zoneId);
},
[dropZones],
setDropZones((prev) => {
const next = new Map(prev);
next.set(zone.id, zone);
return next;
});
},
[setDropZones],
);
const unregisterDropZone = useCallback(
(zoneId: string) => {
setDropZones((prev) => {
if (!prev.has(zoneId)) {
return prev;
}
const next = new Map(prev);
next.delete(zoneId);
return next;
});
},
[setDropZones],

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +38
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
});

useEffect(() => {
if (typeof window === 'undefined') return;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This initial state initializer calls window.matchMedia(...) without checking that matchMedia exists. In environments where window is defined but matchMedia is not, this will throw. Add a window.matchMedia existence guard here and in the effect.

Suggested change
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
});
useEffect(() => {
if (typeof window === 'undefined') return;
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return false;
}
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
});
useEffect(() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;

Copilot uses AI. Check for mistakes.
Comment on lines +105 to +109
if (duration !== undefined) {
style.animationDuration = `${duration}ms`;
}
if (delay !== undefined) {
style.animationDelay = `${delay}ms`;
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useAnimation sets timing via inline styles (animationDuration/animationDelay), which pushes consumers toward style={...} usage. If you want to keep styling Tailwind-driven, consider returning Tailwind duration/delay/ease classes (including arbitrary values) instead of a style object.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +153
setNotifications((prev) => [notification, ...prev]);

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotificationSystemConfig defines maxVisible, but notify currently prepends and keeps all notifications without enforcing this limit. This can lead to unbounded growth of notifications over time. Consider trimming the list in setNotifications based on config.maxVisible (and/or keeping history separately if needed).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants