Repository navigation
Add 8-directional resize system and premium Tailwind design to designer - #41
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in commit 4fd9482. Now correctly using ComponentRegistry.getConfig(currentNode.type) where currentNode is found via findNodeInSchema(schema, resizingNode.nodeId).
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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, | |
| }, |
There was a problem hiding this comment.
Fixed in commit 4fd9482. Removed the aggressive className filtering logic entirely and simplified to only update the style object directly.
| 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 | ||
| } | ||
| }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| - [ ] **Undo/redo for resize operations** (currently works) |
There was a problem hiding this comment.
Fixed in commit 4fd9482. Removed the contradictory undo/redo item from the future enhancements list since it's already implemented and working.
| {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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in commit 4fd9482. Changed function signature to use proper types: (node: SchemaNode, targetId: string): SchemaNode | null with type guards for safe array iteration.
|
@copilot fix |
|
✅ All checks passed!
|
… docs Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
|
✅ All checks passed!
|
…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>
Designer Component Drag and Resize Feature - Code Review Fixes ✅
Fixed Issues from Code Review
1. Type Safety (Canvas.tsx line 209, 273)
schema.typeto use resizing node's type viacurrentNode.typefindNodeInSchemafromanytypes to properSchemaNodetypes2. Unused Variable (Canvas.tsx lines 235-251)
newClassNamevariable3. Cursor Logic (Canvas.tsx line 483)
4. Documentation (DRAG_AND_RESIZE_GUIDE.md line 347)
5. Accessibility (ComponentPalette.tsx lines 143-144)
aria-labelto component cards with resizable statusaria-hidden="true"to decorative badgeAll Review Comments Addressed ✅
Changes Summary:
Tests: All existing tests still passing ✅
Original prompt
💡 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.