Skip to content

Add 8-directional resize system and premium Tailwind design to designer - #41

Merged
huangyiirene merged 8 commits into
mainfrom
copilot/update-draggable-controls
Jan 14, 2026
Merged

huangyiirene merged 8 commits into
mainfrom
copilot/update-draggable-controls

Conversation

Copilot AI commented Jan 14, 2026 •

Copy link
Copy Markdown
Contributor

Designer Component Drag and Resize Feature - Code Review Fixes ✅

Fixed Issues from Code Review

1. Type Safety (Canvas.tsx line 209, 273)

  • ✅ Fixed schema.type to use resizing node's type via currentNode.type
  • ✅ Changed findNodeInSchema from any types to proper SchemaNode types
  • ✅ Added type guards for safe array iteration

2. Unused Variable (Canvas.tsx lines 235-251)

  • ✅ Removed unused newClassName variable
  • ✅ Removed aggressive className filtering logic
  • ✅ Simplified to only update style object

3. Cursor Logic (Canvas.tsx line 483)

  • ✅ Fixed diagonal resize cursor directions
  • ✅ Added proper ne/sw → nesw-resize
  • ✅ Added proper nw/se → nwse-resize
  • ✅ Falls back to ew-resize or ns-resize for edges

4. Documentation (DRAG_AND_RESIZE_GUIDE.md line 347)

  • ✅ Removed contradictory undo/redo from future enhancements
  • ✅ Feature is already implemented and working

5. Accessibility (ComponentPalette.tsx lines 143-144)

  • ✅ Added aria-label to component cards with resizable status
  • ✅ Added aria-hidden="true" to decorative badge
  • ✅ Screen readers now announce "ComponentName (resizable)"

All Review Comments Addressed ✅

Changes Summary:

  • Type-safe implementation throughout
  • Proper cursor feedback for diagonal resize
  • Accessible component palette
  • Clean, maintainable code
  • Accurate documentation

Tests: All existing tests still passing ✅

Original prompt

该如何规划设计器中哪些控件可以拖动,哪些可以放大缩小。如何更新tailwind优化设计体验


💡 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

vercel Bot commented Jan 14, 2026 •

Copy link
Copy Markdown

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

Project Deployment Review Updated (UTC)
objectui-playground Ready Ready Preview, Comment Jan 14, 2026 6:51pm

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 implements a comprehensive drag-and-resize system for the Object UI Designer, addressing the Chinese requirement about planning which controls can be dragged and resized, while optimizing the design experience with premium Tailwind styling.

Changes:

  • Added component resizing capability with 8-directional handles for container components
  • Enhanced visual design with gradient borders, animations, and smooth transitions
  • Implemented resize constraints (min/max width/height) in component metadata
  • Added comprehensive documentation and test coverage

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/types/src/base.ts Extended ComponentMeta interface with resizable properties and constraints
packages/designer/src/index.ts Exported ResizingState type and ResizeHandle components
packages/designer/src/context/DesignerContext.tsx Added resizing state management to designer context
packages/designer/src/components/ResizeHandle.tsx New component implementing 8-directional resize handles
packages/designer/src/components/ComponentPalette.tsx Enhanced with gradient styling and resizable badges
packages/designer/src/components/Canvas.tsx Integrated resize functionality with visual feedback
packages/designer/src/tests/resize.test.tsx Comprehensive test suite for resize functionality
packages/designer/README.md Updated documentation with resize features
packages/designer/IMPLEMENTATION_SUMMARY.zh-CN.md Chinese implementation summary
packages/designer/DRAG_AND_RESIZE_GUIDE.md Detailed guide for drag and resize features
packages/core/src/registry/Registry.ts Added resize metadata to ComponentMeta type
packages/components/src/renderers/layout/grid.tsx Registered grid as resizable with constraints
packages/components/src/renderers/layout/container.tsx Registered container as resizable with constraints
packages/components/src/renderers/layout/card.tsx Registered card as resizable with constraints

}

// Apply constraints from component config
const config = ComponentRegistry.getConfig(schema.type);

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

Using schema.type instead of the resizing node's type. This should be ComponentRegistry.getConfig(currentNode.type) where currentNode is the node being resized (found via resizingNode.nodeId), not the root schema type.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4fd9482. Now correctly using ComponentRegistry.getConfig(currentNode.type) where currentNode is found via findNodeInSchema(schema, resizingNode.nodeId).

Comment on lines +235 to +251
// Update node with new dimensions in className
// We'll add custom width/height to the existing className
const currentNode = findNodeInSchema(schema, resizingNode.nodeId);
if (currentNode) {
const existingClasses = currentNode.className || '';
// Remove any existing width/height classes
let newClassName = existingClasses
.split(' ')
.filter(c => !c.startsWith('w-') && !c.startsWith('h-') && !c.includes('width') && !c.includes('height'))
.join(' ');

// Add inline style instead via data attribute for persistent sizing
updateNode(resizingNode.nodeId, {
style: {
width,
height
}

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The className filter logic is too aggressive and will incorrectly remove unrelated classes. For example, classes like shadow-lg (contains 'w'), hover:bg-gray-100 (contains 'h'), or whitespace-nowrap (contains both) would be removed. Use more specific patterns or consider storing dimensions in the style object only.

Suggested change
// Update node with new dimensions in className
// We'll add custom width/height to the existing className
const currentNode = findNodeInSchema(schema, resizingNode.nodeId);
if (currentNode) {
const existingClasses = currentNode.className || '';
// Remove any existing width/height classes
let newClassName = existingClasses
.split(' ')
.filter(c => !c.startsWith('w-') && !c.startsWith('h-') && !c.includes('width') && !c.includes('height'))
.join(' ');
// Add inline style instead via data attribute for persistent sizing
updateNode(resizingNode.nodeId, {
style: {
width,
height
}
// Persist new dimensions via style on the node schema
const currentNode = findNodeInSchema(schema, resizingNode.nodeId);
if (currentNode) {
updateNode(resizingNode.nodeId, {
style: {
width,
height,
},

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4fd9482. Removed the aggressive className filtering logic entirely and simplified to only update the style object directly.

Comment on lines +237 to +252
const currentNode = findNodeInSchema(schema, resizingNode.nodeId);
if (currentNode) {
const existingClasses = currentNode.className || '';
// Remove any existing width/height classes
let newClassName = existingClasses
.split(' ')
.filter(c => !c.startsWith('w-') && !c.startsWith('h-') && !c.includes('width') && !c.includes('height'))
.join(' ');

// Add inline style instead via data attribute for persistent sizing
updateNode(resizingNode.nodeId, {
style: {
width,
height
}
});

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The variable newClassName is computed but never used. Since the comment indicates using inline style instead, remove the className manipulation code (lines 238-243) and keep only the style update.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4fd9482. Removed the unused newClassName variable and all className manipulation code (lines 238-243).

/* Resizing state cursor override */
${resizingNode ? `
* {
cursor: ${resizingNode.direction.includes('e') || resizingNode.direction.includes('w') ? 'ew-resize' : 'ns-resize'} !important;

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The cursor logic is incomplete for diagonal resize directions. When resizing diagonally (ne, nw, se, sw), the cursor should be 'nesw-resize' or 'nwse-resize', not 'ew-resize' or 'ns-resize'. This should check for corner directions first before falling back to edge directions.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4fd9482. Added proper cursor logic for diagonal directions:

  • ne/sw → nesw-resize
  • nw/se → nwse-resize
  • e/w → ew-resize
  • n/s → ns-resize

- [ ] **Keyboard resize** (Arrow keys with Shift)
- [ ] **Percentage-based constraints**
- [ ] **Resize preview overlay** showing dimensions
- [ ] **Undo/redo for resize operations** (currently works)

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

Line 347 is contradictory - it lists 'Undo/redo for resize operations' as a future enhancement while stating '(currently works)'. If the feature is already implemented, it should not be in the future enhancements list.

Suggested change
- [ ] **Undo/redo for resize operations** (currently works)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4fd9482. Removed the contradictory undo/redo item from the future enhancements list since it's already implemented and working.

Comment on lines +143 to +144
{isResizable && (
<div className="absolute top-1 right-1 w-2 h-2 bg-gradient-to-br from-emerald-400 to-green-500 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-sm ring-2 ring-white" title="Resizable"></div>

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The resizable badge is a visual-only indicator with no accessible alternative. Screen reader users won't know a component is resizable. Consider adding an aria-label to the parent element or including text with sr-only class.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4fd9482. Added aria-label to the component card with resizable status (e.g., "Card (resizable)") and aria-hidden="true" to the decorative badge.

}, [resizingNode, setResizingNode, updateNode, schema]);

// Helper to find node in schema
const findNodeInSchema = (node: any, targetId: string): any => {

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The function uses any types instead of proper TypeScript types. Use SchemaNode for the node parameter and return type to maintain type safety throughout the codebase.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 4fd9482. Changed function signature to use proper types: (node: SchemaNode, targetId: string): SchemaNode | null with type guards for safe array iteration.

@huangyiirene

Copy link
Copy Markdown
Collaborator

@copilot fix

Copilot AI changed the title [WIP] Update draggable controls and resizing options in designer Add 8-directional resize system and premium Tailwind design to designer Jan 14, 2026
Copilot AI requested a review from huangyiirene January 14, 2026 18:48
@github-actions

Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

… docs

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ All checks passed!

  • ✅ Type check passed
  • ✅ Tests passed
  • ✅ Lint check completed

@huangyiirene
huangyiirene marked this pull request as ready for review January 14, 2026 18:55
@huangyiirene
huangyiirene merged commit aecd3a0 into main Jan 14, 2026
7 checks passed
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 9, 2026
…ativeKanban* trio retires (objectui#7664) (objectstack-ai#7743)

* feat(types): the `kanban` arm declares the plugin dialect; the DeclarativeKanban* trio retires

For an authored `type: 'kanban'` document two types were authoritative
depending on who asked: `safeValidateSchema` honoured `DeclarativeKanbanSchema`
while the renderer registered for the key consumed `@object-ui/plugin-kanban`'s
own `KanbanSchema`, so a board could validate and render empty. Maintainer
ruling (a) (2026-09-05, batch objectstack-ai#41): the plugin dialect is authoritative.

- `@object-ui/types` declares `KanbanSchema` / `KanbanColumn` / `KanbanCard` /
  `CardTemplate` / `ColumnWidthConfig` (member for member the plugin's shape),
  mirrors them in `zod/complex.zod.ts`, and `SchemaRegistry['kanban']` names
  the declaration; `plugin-kanban/src/types.ts` re-exports the five.
- The retired dialect's inert keys (`draggable`, column `color`) are `?: never`
  tombstones refused by name; `onColumnAdd` / `onCardAdd` carried over.
- The six `DeclarativeKanban*` exports retire (ADR-0049) with their two
  objectui#7645 pins; a 7664 pin on each side replaces them. The parity,
  handler-key and docs-retired ratchets are re-keyed; the docs page moves to
  the plugin dialect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3

* fix(types): the `kanban` arm refuses `onCardClick` by name, not by omission

The successor `'kanban'` arm dropped `onCardClick` instead of giving it a
objectstack-ai#6124 disposition. `BaseSchema` is `.passthrough()`, so a dropped key is not
refused — it stops being judged and the value is KEPT. Measured on the built
dist before this commit, `{ type: 'kanban', columns: [], onCardClick: {
action: 'toast' } }` was ACCEPTED with `{"action":"toast"}` surviving into the
parsed output, while the same document at `onCardMove` / `onQuickAdd` /
`onColumnAdd` / `draggable` was REFUSED. After it, all five are REFUSED.

The rationale that was recorded for the deletion — "the object-bound board owns
the click" — is contradicted by the source. Measured per registration
(`plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx`, new here):

  - `'kanban-ui'`: `KanbanRenderer` forwards `onCardClick`, `onCardMove` and
    `onQuickAdd` to the board by identity, from one block. Lit controls: the
    two keys already ruled runtime slots come out live on this probe.
  - `'kanban'` / `'object-kanban'`: `ObjectKanban` substitutes its own function
    for `onCardClick` — and for `onCardMove`, in the same object literal, with
    `onQuickAdd` arriving by identity as the lit control on this key. The two
    keys have identical reachability here, so that reading retires both or
    neither.
  - and the substitute CALLS the authored handler: `ObjectKanban` declares an
    `onCardClick` prop (there is no `onCardMove` prop), which `SchemaRenderer`
    supplies by spreading non-metadata schema keys. An `onCardClick` authored
    on a `type: 'kanban'` document runs.

So the disposition is RUNTIME SLOT: callable on the TypeScript face, refused by
name on the mirror. `RUNTIME_SLOT` 44 -> 45, `ALL_SITES` 66 -> 67; `RETIRED`
stays 22 and the objectstack-ai#7340 docs census stays 26.

Why no ratchet caught it: the objectstack-ai#6124 ledger is hand-written by key, so a
substitution holds its length constant and a deleted key is invisible to it.
The new file derives the required key set from the read site instead — it
extracts the `schema.on*` reads out of `KanbanRenderer`'s body and requires
each to be a declared arm member, which is red on exactly this deletion.

Also in this commit, riding with the remediation:

  - the `@object-ui/plugin-kanban` changeset no longer claims the shape is
    "member for member what this package declared". Counted with the TypeScript
    parser against `origin/main`, it is that shape plus four members: three
    `?: never` tombstones, and `onCardClick`, which the plugin dialect never
    declared while its renderer read it.
  - the `@object-ui/types` changeset gains the accept-set bullet for the five
    handler arms.
  - the `draggable` / column `color` tombstone JSDoc cites the two-prong
    discriminator instead of arguing from inertness. Re-measured on
    `origin/main`: `schema-reference.md` opened its kanban example with
    `"draggable": true`, documented it in the property table, and authored a
    `color` on every one of its three columns.
  - `schema-reference.md` documents `onCardClick` again, and it joins the objectstack-ai#7340
    blanket-sweep control (six rows -> seven).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3

* fix(plugin-kanban): the new reachability test compiles under the package's own type-check

`Type Check` went red on the previous head, in `packages/plugin-kanban`'s own
`pnpm run type-check`, on the file the remediation added. Two distinct causes,
both reproduced with that exact command before and after:

  - TS2591 ×3 (`node:fs` / `node:path` / `node:url`) — `tsconfig.test.json`
    names `types`, which switches off automatic `@types/*` inclusion, and its
    comment recorded the invariant the new file breaks: "nothing in these tests
    touches Node globals". The file reads `../index.tsx` off disk on purpose —
    that is what makes the guard derive the forwarded key set from the read
    site instead of from a list. `node` is named alongside
    `@testing-library/jest-dom`, exactly as plugin-calendar / -charts /
    -chatbot / -dashboard / -detail / -gantt / -grid already do, and the
    comment now says why. It stays OUT of `tsconfig.json`: package source ships
    to browsers.
  - TS2741 ×2 — `SchemaRendererProvider` requires `dataSource`. Spelled
    `dataSource={undefined}`, the form three other call sites in this repo use
    for a surface that fetches nothing; these boards author their lanes
    statically, so it is the same runtime value the measurements were taken
    with. ⛔ Not fixed by loosening the prop or casting the provider — the
    subject of this PR is that the type surface must be honest.

`packages/plugin-kanban$ pnpm run type-check` exit 2 -> 0, and
`turbo run type-check` is 81/81 across the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3

* test(types): drop the two WiderThanDeclared rows keyed on retired DeclarativeKanban pairs

The mirror-wider-than-declared ledger (added by PR 7758) keys two of its 36
rows on pair names this branch retires:

  'complex.zod.ts#DeclarativeKanbanColumnSchema': 'cards'
  'complex.zod.ts#DeclarativeKanbanSchema': 'columns'

`assertionWiderLedgerKeysAreRegistered` requires every ledger key to be a
registered `MirrorKey`. After the re-key to `KanbanColumnSchema` /
`KanbanSchema`, these two no longer are, and the merged tree is tsc-RED with
exactly that one diagnostic.

Deleting the rows is the ledger's own documented remedy 4 for a pair that has
gone: "a key DISAPPEARED - the faces agree again; correct or delete the
entry." Re-keying them to the new pair names instead was measured RED:
`WiderOf` for both new pairs is `never`, because the re-key eliminated the
drift those rows recorded. No coverage is lost - you cannot watch a pair that
no longer exists.

Redness inherited from two correct changes landing in sequence, not
introduced here. Ruled by the domain:ui PM seat on PR 7743.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 4fd94821 Deployed Jan 14, 2026 by vercel[bot]
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