@ericrisco/rsc 3.3.1 → 3.4.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/README.md +2 -2
- package/manifest.json +43 -22
- package/package.json +1 -1
- package/scripts/lib/default-skill-floor.js +5 -1
- package/scripts/lib/domains.js +2 -2
- package/scripts/lib/retired-skills.js +4 -0
- package/skills/design/SKILL.md +4 -3
- package/skills/design/evals/cases.yaml +2 -2
- package/skills/react/SKILL.md +2 -1
- package/skills/ux/SKILL.md +149 -0
- package/skills/ux/THIRD_PARTY.md +32 -0
- package/skills/ux/evals/cases.yaml +40 -0
- package/skills/ux/references/flows.md +124 -0
- package/skills/ux/upstream/UPSTREAM.json +59 -0
- package/skills/ux/upstream/ux-designer/GUIDE.md +180 -0
- package/skills/ux/upstream/ux-designer/LICENSE +21 -0
- package/skills/ux/upstream/ux-designer/references/01-core-principles.md +251 -0
- package/skills/ux/upstream/ux-designer/references/02-laws-of-ux.md +330 -0
- package/skills/ux/upstream/ux-designer/references/03-accessibility.md +411 -0
- package/skills/ux/upstream/ux-designer/references/04-visual-design.md +433 -0
- package/skills/ux/upstream/ux-designer/references/05-information-architecture.md +432 -0
- package/skills/ux/upstream/ux-designer/references/06-interaction-design.md +495 -0
- package/skills/ux/upstream/ux-designer/references/07-forms-and-inputs.md +491 -0
- package/skills/ux/upstream/ux-designer/references/08-mobile-ux.md +539 -0
- package/skills/ux/upstream/ux-designer/references/09-ux-writing.md +493 -0
- package/skills/ux/upstream/ux-designer/references/10-user-research.md +533 -0
- package/skills/ux/upstream/ux-designer/references/11-design-systems.md +618 -0
- package/skills/ux/upstream/ux-designer/references/12a-presence-awareness.md +395 -0
- package/skills/ux/upstream/ux-designer/references/12b-conflict-resolution-sync.md +420 -0
- package/skills/ux/upstream/ux-designer/references/13a-canvas-navigation.md +388 -0
- package/skills/ux/upstream/ux-designer/references/13b-canvas-objects-performance.md +559 -0
- package/skills/ux/upstream/ux-designer/references/14-ai-ux-patterns.md +511 -0
- package/skills/ux/upstream/ux-designer/references/15-ethical-design.md +419 -0
- package/skills/ux/upstream/ux-designer/references/16-onboarding.md +430 -0
- package/skills/ux/upstream/ux-designer/references/17-notifications.md +461 -0
- package/skills/ux/upstream/ux-designer/references/18-data-visualization.md +453 -0
- package/skills/ux/upstream/ux-designer/references/19-search-ux.md +479 -0
- package/skills/ux/upstream/ux-designer/references/20-emotional-design.md +382 -0
- package/skills/ux/upstream/ux-designer/references/21-data-tables.md +629 -0
- package/skills/ux/upstream/ux-designer/references/22-performance-ux.md +626 -0
- package/skills/ux/upstream/ux-designer/references/23-internationalization.md +347 -0
- package/skills/ux/upstream/ux-designer/references/24-voice-and-multimodal.md +308 -0
- package/skills/ux/upstream/ux-designer/references/25a-barcode-camera-scanning.md +342 -0
- package/skills/ux/upstream/ux-designer/references/25b-barcode-scanner-workflows.md +412 -0
- package/skills/ux/upstream/ux-designer/references/25c-barcode-display.md +277 -0
- package/skills/ux/upstream/ux-usability-foundations/GUIDE.md +285 -0
- package/skills/ux/upstream/ux-usability-foundations/LICENSE +21 -0
- package/skills/ux/upstream/ux-usability-foundations/references/anti-patterns.md +267 -0
- package/skills/ux/upstream/ux-usability-foundations/references/checklists.md +130 -0
- package/skills/ux/upstream/ux-usability-foundations/references/decision-prompts.md +340 -0
- package/skills/ux/upstream/ux-usability-foundations/references/principle-cards.md +449 -0
- package/targets/clone-bootstrap.mjs +1 -1
- package/skills/ui-engineering/SKILL.md +0 -113
- package/skills/ui-engineering/evals/cases.yaml +0 -38
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Canvas Apps: Navigation & Interaction
|
|
2
|
+
|
|
3
|
+
Canvas-based interfaces provide an infinite 2D workspace where users can freely place, move, and manipulate objects. Found in design tools (Figma, Sketch), whiteboards (Miro, FigJam), maps, and diagramming apps.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- Canvas Navigation
|
|
10
|
+
- Object Selection
|
|
11
|
+
- Object Manipulation
|
|
12
|
+
- Key Metrics
|
|
13
|
+
- Anti-Patterns
|
|
14
|
+
- Sources
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Canvas Navigation
|
|
19
|
+
|
|
20
|
+
### Coordinate Systems
|
|
21
|
+
|
|
22
|
+
A canvas app has two coordinate systems:
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
// Screen coordinates: pixel position on display
|
|
26
|
+
const screenPoint = { x: 500, y: 300 };
|
|
27
|
+
|
|
28
|
+
// Canvas coordinates: position in the infinite canvas space
|
|
29
|
+
const canvasPoint = {
|
|
30
|
+
x: (screenPoint.x - camera.x) / camera.zoom,
|
|
31
|
+
y: (screenPoint.y - camera.y) / camera.zoom
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Camera state
|
|
35
|
+
const camera = {
|
|
36
|
+
x: 0, // Horizontal offset
|
|
37
|
+
y: 0, // Vertical offset
|
|
38
|
+
zoom: 1 // Scale factor (1 = 100%)
|
|
39
|
+
};
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Zoom UI
|
|
43
|
+
|
|
44
|
+
Zoom should feel natural, focusing on the cursor position.
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
function handleZoom(event) {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
|
|
50
|
+
const zoomFactor = event.deltaY > 0 ? 0.9 : 1.1;
|
|
51
|
+
const newZoom = clamp(camera.zoom * zoomFactor, MIN_ZOOM, MAX_ZOOM);
|
|
52
|
+
|
|
53
|
+
// Zoom toward cursor position
|
|
54
|
+
const cursorX = event.clientX;
|
|
55
|
+
const cursorY = event.clientY;
|
|
56
|
+
|
|
57
|
+
// Calculate new camera position to keep cursor point fixed
|
|
58
|
+
camera.x = cursorX - (cursorX - camera.x) * (newZoom / camera.zoom);
|
|
59
|
+
camera.y = cursorY - (cursorY - camera.y) * (newZoom / camera.zoom);
|
|
60
|
+
camera.zoom = newZoom;
|
|
61
|
+
|
|
62
|
+
render();
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Zoom Controls
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
┌──────────────────────────────┐
|
|
70
|
+
│ [−] ━━━●━━━━━━━━━ [+] │ Slider
|
|
71
|
+
│ 100% │ Current level
|
|
72
|
+
│ [Fit] [50%] [100%] [200%] │ Presets
|
|
73
|
+
└──────────────────────────────┘
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Control | Action |
|
|
77
|
+
|---------|--------|
|
|
78
|
+
| Scroll wheel | Zoom in/out |
|
|
79
|
+
| Ctrl/Cmd + scroll | Zoom (alternative) |
|
|
80
|
+
| Pinch gesture | Zoom on touch devices |
|
|
81
|
+
| +/- keys | Step zoom |
|
|
82
|
+
| 0 or 1 | Reset to 100% |
|
|
83
|
+
| Shift + 1 | Fit all to view |
|
|
84
|
+
| Shift + 2 | Fit selection to view |
|
|
85
|
+
|
|
86
|
+
#### Zoom Levels
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Typical range: 10% to 4000%+
|
|
90
|
+
Common presets: 25%, 50%, 75%, 100%, 150%, 200%, 400%
|
|
91
|
+
|
|
92
|
+
Semantic zooming (content changes at different levels):
|
|
93
|
+
- 10-25% → Show only shapes/outlines
|
|
94
|
+
- 25-50% → Show basic content, hide details
|
|
95
|
+
- 50-100% → Full content visible
|
|
96
|
+
- 100-200% → Pixel-perfect editing
|
|
97
|
+
- 200%+ → Sub-pixel precision work
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Pan/Scroll
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
// Space + drag to pan
|
|
104
|
+
let isPanning = false;
|
|
105
|
+
let lastPanPoint = { x: 0, y: 0 };
|
|
106
|
+
|
|
107
|
+
function handleKeyDown(event) {
|
|
108
|
+
if (event.code === 'Space' && !isPanning) {
|
|
109
|
+
isPanning = true;
|
|
110
|
+
document.body.style.cursor = 'grab';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleMouseMove(event) {
|
|
115
|
+
if (isPanning && event.buttons === 1) {
|
|
116
|
+
camera.x += event.clientX - lastPanPoint.x;
|
|
117
|
+
camera.y += event.clientY - lastPanPoint.y;
|
|
118
|
+
document.body.style.cursor = 'grabbing';
|
|
119
|
+
render();
|
|
120
|
+
}
|
|
121
|
+
lastPanPoint = { x: event.clientX, y: event.clientY };
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Pan Methods
|
|
126
|
+
|
|
127
|
+
| Method | Trigger | Notes |
|
|
128
|
+
|--------|---------|-------|
|
|
129
|
+
| Space + drag | Hold space, click and drag | Most common in design tools |
|
|
130
|
+
| Middle mouse | Middle click and drag | Power user shortcut |
|
|
131
|
+
| Two-finger drag | Trackpad gesture | Natural for laptop users |
|
|
132
|
+
| Scroll bars | Click and drag | Visible scroll position |
|
|
133
|
+
| Arrow keys | Keyboard navigation | Step-based movement |
|
|
134
|
+
|
|
135
|
+
### Minimap / Overview
|
|
136
|
+
|
|
137
|
+
Bird's-eye navigation for large canvases.
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<div class="minimap">
|
|
141
|
+
<canvas id="minimap-canvas"></canvas>
|
|
142
|
+
<div class="minimap__viewport"></div>
|
|
143
|
+
</div>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```css
|
|
147
|
+
.minimap {
|
|
148
|
+
position: fixed;
|
|
149
|
+
bottom: 16px;
|
|
150
|
+
right: 16px;
|
|
151
|
+
width: 200px;
|
|
152
|
+
height: 150px;
|
|
153
|
+
background: #f3f4f6;
|
|
154
|
+
border: 1px solid #d1d5db;
|
|
155
|
+
border-radius: 8px;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.minimap__viewport {
|
|
160
|
+
position: absolute;
|
|
161
|
+
border: 2px solid #3b82f6;
|
|
162
|
+
background: rgba(59, 130, 246, 0.1);
|
|
163
|
+
cursor: move;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Minimap Guidelines:**
|
|
168
|
+
- Show at 5-10% of actual canvas scale
|
|
169
|
+
- Highlight current viewport area
|
|
170
|
+
- Allow click-to-navigate
|
|
171
|
+
- Allow drag viewport rectangle
|
|
172
|
+
- Consider hiding when zoomed to fit all
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Object Selection
|
|
177
|
+
|
|
178
|
+
### Selection Modes
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
// Click selection
|
|
182
|
+
function handleClick(event) {
|
|
183
|
+
const hitObject = hitTest(event.x, event.y);
|
|
184
|
+
|
|
185
|
+
if (event.shiftKey) {
|
|
186
|
+
// Add to / remove from selection
|
|
187
|
+
toggleSelection(hitObject);
|
|
188
|
+
} else {
|
|
189
|
+
// Replace selection
|
|
190
|
+
clearSelection();
|
|
191
|
+
if (hitObject) select(hitObject);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Marquee selection
|
|
196
|
+
function handleMarquee(startPoint, endPoint) {
|
|
197
|
+
const rect = {
|
|
198
|
+
x: Math.min(startPoint.x, endPoint.x),
|
|
199
|
+
y: Math.min(startPoint.y, endPoint.y),
|
|
200
|
+
width: Math.abs(endPoint.x - startPoint.x),
|
|
201
|
+
height: Math.abs(endPoint.y - startPoint.y)
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const contained = objects.filter(obj =>
|
|
205
|
+
isContainedIn(obj.bounds, rect)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (event.shiftKey) {
|
|
209
|
+
addToSelection(contained);
|
|
210
|
+
} else {
|
|
211
|
+
setSelection(contained);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Selection Visuals
|
|
217
|
+
|
|
218
|
+
```css
|
|
219
|
+
/* Selected object */
|
|
220
|
+
.object--selected {
|
|
221
|
+
outline: 2px solid #3b82f6;
|
|
222
|
+
outline-offset: 2px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* Selection marquee */
|
|
226
|
+
.selection-marquee {
|
|
227
|
+
position: absolute;
|
|
228
|
+
border: 1px solid #3b82f6;
|
|
229
|
+
background: rgba(59, 130, 246, 0.1);
|
|
230
|
+
pointer-events: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Multi-selection bounding box */
|
|
234
|
+
.selection-bounds {
|
|
235
|
+
position: absolute;
|
|
236
|
+
border: 1px dashed #3b82f6;
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Selection Keyboard Shortcuts
|
|
241
|
+
|
|
242
|
+
| Shortcut | Action |
|
|
243
|
+
|----------|--------|
|
|
244
|
+
| Click | Select single object |
|
|
245
|
+
| Shift + Click | Add/remove from selection |
|
|
246
|
+
| Cmd/Ctrl + A | Select all |
|
|
247
|
+
| Escape | Deselect all |
|
|
248
|
+
| Tab | Select next object |
|
|
249
|
+
| Shift + Tab | Select previous object |
|
|
250
|
+
| Cmd/Ctrl + Click | Deep select (nested elements) |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Object Manipulation
|
|
255
|
+
|
|
256
|
+
### Transform Controls
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
┌─────────────────────────────────┐
|
|
260
|
+
◯───┤ ├───◯ ← Rotation handle
|
|
261
|
+
│ │
|
|
262
|
+
◻───┤ Selected Object ├───◻ ← Resize handles
|
|
263
|
+
│ │
|
|
264
|
+
◯───┤ ├───◯
|
|
265
|
+
└─────────────────────────────────┘
|
|
266
|
+
◻ ◻
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```css
|
|
270
|
+
/* Resize handles */
|
|
271
|
+
.resize-handle {
|
|
272
|
+
position: absolute;
|
|
273
|
+
width: 8px;
|
|
274
|
+
height: 8px;
|
|
275
|
+
background: white;
|
|
276
|
+
border: 1px solid #3b82f6;
|
|
277
|
+
border-radius: 2px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* Corner handles - resize */
|
|
281
|
+
.resize-handle--nw { cursor: nwse-resize; top: -4px; left: -4px; }
|
|
282
|
+
.resize-handle--ne { cursor: nesw-resize; top: -4px; right: -4px; }
|
|
283
|
+
.resize-handle--sw { cursor: nesw-resize; bottom: -4px; left: -4px; }
|
|
284
|
+
.resize-handle--se { cursor: nwse-resize; bottom: -4px; right: -4px; }
|
|
285
|
+
|
|
286
|
+
/* Edge handles */
|
|
287
|
+
.resize-handle--n { cursor: ns-resize; top: -4px; left: 50%; }
|
|
288
|
+
.resize-handle--s { cursor: ns-resize; bottom: -4px; left: 50%; }
|
|
289
|
+
.resize-handle--e { cursor: ew-resize; right: -4px; top: 50%; }
|
|
290
|
+
.resize-handle--w { cursor: ew-resize; left: -4px; top: 50%; }
|
|
291
|
+
|
|
292
|
+
/* Rotation handle */
|
|
293
|
+
.rotate-handle {
|
|
294
|
+
position: absolute;
|
|
295
|
+
top: -30px;
|
|
296
|
+
left: 50%;
|
|
297
|
+
width: 10px;
|
|
298
|
+
height: 10px;
|
|
299
|
+
background: white;
|
|
300
|
+
border: 1px solid #3b82f6;
|
|
301
|
+
border-radius: 50%;
|
|
302
|
+
cursor: grab;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Drag and Drop
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
function handleDrag(object, event) {
|
|
310
|
+
// Show ghost/preview at cursor
|
|
311
|
+
showDragPreview(object, event.x, event.y);
|
|
312
|
+
|
|
313
|
+
// Check drop targets
|
|
314
|
+
const dropTarget = findDropTarget(event.x, event.y);
|
|
315
|
+
if (dropTarget) {
|
|
316
|
+
highlightDropTarget(dropTarget);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function handleDrop(object, event) {
|
|
321
|
+
const dropTarget = findDropTarget(event.x, event.y);
|
|
322
|
+
|
|
323
|
+
// Animate to final position
|
|
324
|
+
animateTo(object, {
|
|
325
|
+
x: snapToGrid(event.x),
|
|
326
|
+
y: snapToGrid(event.y)
|
|
327
|
+
}, { duration: 100 });
|
|
328
|
+
|
|
329
|
+
hideDragPreview();
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Drag and Drop Guidelines:**
|
|
334
|
+
- Show visual feedback during drag (elevation, shadow)
|
|
335
|
+
- Animate other elements moving out of the way
|
|
336
|
+
- Use 100ms animation for drop
|
|
337
|
+
- Provide clear drop zone indicators
|
|
338
|
+
- Support keyboard alternatives (WCAG 2.5.7)
|
|
339
|
+
|
|
340
|
+
### Keyboard Movement
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
const MOVE_STEP = 1; // Arrow key
|
|
344
|
+
const MOVE_STEP_LARGE = 10; // Shift + Arrow
|
|
345
|
+
|
|
346
|
+
function handleArrowKey(key, shiftKey) {
|
|
347
|
+
const step = shiftKey ? MOVE_STEP_LARGE : MOVE_STEP;
|
|
348
|
+
|
|
349
|
+
selectedObjects.forEach(obj => {
|
|
350
|
+
switch(key) {
|
|
351
|
+
case 'ArrowUp': obj.y -= step; break;
|
|
352
|
+
case 'ArrowDown': obj.y += step; break;
|
|
353
|
+
case 'ArrowLeft': obj.x -= step; break;
|
|
354
|
+
case 'ArrowRight': obj.x += step; break;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Key Metrics
|
|
363
|
+
|
|
364
|
+
| Metric | Value | Context |
|
|
365
|
+
|--------|-------|---------|
|
|
366
|
+
| Zoom range | 10% - 4000% | Typical design tools |
|
|
367
|
+
| Pan speed | 1:1 cursor ratio | Natural feeling |
|
|
368
|
+
| Touch target | 44×44px | Minimum for handles |
|
|
369
|
+
| Render target | 60fps | During pan/zoom |
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## Anti-Patterns
|
|
374
|
+
|
|
375
|
+
1. **Fixed canvas size** - Limiting creativity with artificial boundaries
|
|
376
|
+
2. **Zoom at screen center** - Disorienting; zoom at cursor instead
|
|
377
|
+
3. **Sluggish pan/zoom** - Must feel instant and smooth
|
|
378
|
+
4. **Inconsistent shortcuts** - Differs from industry standard
|
|
379
|
+
5. **Poor touch support** - Gestures should match platform conventions
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Sources
|
|
384
|
+
|
|
385
|
+
- [Steve Ruiz - Creating a Zoom UI](https://www.steveruiz.me/posts/zoom-ui)
|
|
386
|
+
- [NN/g - Drag and Drop](https://www.nngroup.com/articles/drag-drop/)
|
|
387
|
+
- [Pencil & Paper - Drag & Drop UX Patterns](https://www.pencilandpaper.io/articles/ux-pattern-drag-and-drop)
|
|
388
|
+
- [Salesforce - 4 Patterns for Accessible Drag and Drop](https://medium.com/salesforce-ux/4-major-patterns-for-accessible-drag-and-drop-1d43f64ebf09)
|