@ericrisco/rsc 3.3.0 → 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 +6 -3
- 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/suggest/SKILL.md +12 -8
- package/skills/suggest/evals/cases.yaml +6 -0
- 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,559 @@
|
|
|
1
|
+
# Canvas Apps: Objects, Performance & Patterns
|
|
2
|
+
|
|
3
|
+
This reference covers smart guides, layer management, canvas elements, performance optimization, whiteboard-specific patterns, and accessibility for canvas-based applications.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- Smart Guides & Snapping
|
|
10
|
+
- Layers & Hierarchy
|
|
11
|
+
- Canvas Elements
|
|
12
|
+
- Performance Optimization
|
|
13
|
+
- Whiteboard-Specific Patterns
|
|
14
|
+
- Accessibility
|
|
15
|
+
- Key Metrics
|
|
16
|
+
- Anti-Patterns
|
|
17
|
+
- Sources
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Smart Guides & Snapping
|
|
22
|
+
|
|
23
|
+
### Alignment Guides
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
function calculateAlignmentGuides(draggedObject, allObjects) {
|
|
27
|
+
const guides = [];
|
|
28
|
+
const threshold = 5; // Snap within 5px
|
|
29
|
+
|
|
30
|
+
allObjects.forEach(other => {
|
|
31
|
+
if (other === draggedObject) return;
|
|
32
|
+
|
|
33
|
+
// Horizontal center alignment
|
|
34
|
+
if (Math.abs(draggedObject.centerY - other.centerY) < threshold) {
|
|
35
|
+
guides.push({
|
|
36
|
+
type: 'horizontal',
|
|
37
|
+
y: other.centerY,
|
|
38
|
+
from: Math.min(draggedObject.left, other.left),
|
|
39
|
+
to: Math.max(draggedObject.right, other.right)
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Vertical center alignment
|
|
44
|
+
if (Math.abs(draggedObject.centerX - other.centerX) < threshold) {
|
|
45
|
+
guides.push({
|
|
46
|
+
type: 'vertical',
|
|
47
|
+
x: other.centerX,
|
|
48
|
+
from: Math.min(draggedObject.top, other.top),
|
|
49
|
+
to: Math.max(draggedObject.bottom, other.bottom)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Edge alignments (top, bottom, left, right)
|
|
54
|
+
// ... similar checks for edges
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return guides;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Smart Guide Visuals
|
|
62
|
+
|
|
63
|
+
```css
|
|
64
|
+
/* Alignment guides */
|
|
65
|
+
.smart-guide {
|
|
66
|
+
position: absolute;
|
|
67
|
+
background: #f43f5e;
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
z-index: 1000;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.smart-guide--horizontal {
|
|
73
|
+
height: 1px;
|
|
74
|
+
width: 100%;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.smart-guide--vertical {
|
|
78
|
+
width: 1px;
|
|
79
|
+
height: 100%;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Distance indicator */
|
|
83
|
+
.distance-indicator {
|
|
84
|
+
position: absolute;
|
|
85
|
+
padding: 2px 6px;
|
|
86
|
+
background: #f43f5e;
|
|
87
|
+
color: white;
|
|
88
|
+
font-size: 11px;
|
|
89
|
+
border-radius: 3px;
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Grid Snapping
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const GRID_SIZE = 8; // 8px grid
|
|
98
|
+
|
|
99
|
+
function snapToGrid(value) {
|
|
100
|
+
return Math.round(value / GRID_SIZE) * GRID_SIZE;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function snapPosition(x, y) {
|
|
104
|
+
return {
|
|
105
|
+
x: snapToGrid(x),
|
|
106
|
+
y: snapToGrid(y)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Snapping Options
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
┌─────────────────────────────────────────────┐
|
|
115
|
+
│ Snapping (⌘;) [Toggle] │
|
|
116
|
+
├─────────────────────────────────────────────┤
|
|
117
|
+
│ ✓ Snap to grid │
|
|
118
|
+
│ ✓ Snap to objects │
|
|
119
|
+
│ ✓ Snap to guides │
|
|
120
|
+
│ Snap to pixel │
|
|
121
|
+
├─────────────────────────────────────────────┤
|
|
122
|
+
│ Grid size: [8px ▼] │
|
|
123
|
+
└─────────────────────────────────────────────┘
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Layers & Hierarchy
|
|
129
|
+
|
|
130
|
+
### Layer Panel
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
┌─────────────────────────────────────────────────────┐
|
|
134
|
+
│ Layers [+] [⋮] │
|
|
135
|
+
├─────────────────────────────────────────────────────┤
|
|
136
|
+
│ ▼ 📁 Header 👁 🔒 │
|
|
137
|
+
│ │ ├─ 📄 Logo 👁 🔓 │
|
|
138
|
+
│ │ ├─ 📄 Navigation 👁 🔓 │
|
|
139
|
+
│ │ └─ 📄 Search 👁 🔓 │
|
|
140
|
+
│ ▶ 📁 Main Content 👁 🔓 │
|
|
141
|
+
│ ▶ 📁 Footer 👁 🔓 │
|
|
142
|
+
│ 📄 Background 👁 🔓 │
|
|
143
|
+
└─────────────────────────────────────────────────────┘
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```css
|
|
147
|
+
/* Layer item */
|
|
148
|
+
.layer-item {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
padding: 4px 8px;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.layer-item:hover {
|
|
156
|
+
background: #f3f4f6;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.layer-item--selected {
|
|
160
|
+
background: #dbeafe;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.layer-item--dragging {
|
|
164
|
+
opacity: 0.5;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* Indent for hierarchy */
|
|
168
|
+
.layer-item[data-depth="1"] { padding-left: 20px; }
|
|
169
|
+
.layer-item[data-depth="2"] { padding-left: 36px; }
|
|
170
|
+
.layer-item[data-depth="3"] { padding-left: 52px; }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Z-Index Operations
|
|
174
|
+
|
|
175
|
+
| Shortcut | Action |
|
|
176
|
+
|----------|--------|
|
|
177
|
+
| Cmd/Ctrl + ] | Bring forward |
|
|
178
|
+
| Cmd/Ctrl + [ | Send backward |
|
|
179
|
+
| Cmd/Ctrl + Shift + ] | Bring to front |
|
|
180
|
+
| Cmd/Ctrl + Shift + [ | Send to back |
|
|
181
|
+
|
|
182
|
+
### Grouping
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
function groupSelection() {
|
|
186
|
+
const group = {
|
|
187
|
+
type: 'group',
|
|
188
|
+
id: generateId(),
|
|
189
|
+
children: [...selectedObjects],
|
|
190
|
+
bounds: calculateBounds(selectedObjects)
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Remove from root, add to group
|
|
194
|
+
selectedObjects.forEach(obj => {
|
|
195
|
+
removeFromCanvas(obj);
|
|
196
|
+
obj.parent = group;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
addToCanvas(group);
|
|
200
|
+
setSelection([group]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function ungroupSelection() {
|
|
204
|
+
selectedObjects.forEach(group => {
|
|
205
|
+
if (group.type !== 'group') return;
|
|
206
|
+
|
|
207
|
+
group.children.forEach(child => {
|
|
208
|
+
child.parent = null;
|
|
209
|
+
addToCanvas(child);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
removeFromCanvas(group);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| Shortcut | Action |
|
|
218
|
+
|----------|--------|
|
|
219
|
+
| Cmd/Ctrl + G | Group selection |
|
|
220
|
+
| Cmd/Ctrl + Shift + G | Ungroup selection |
|
|
221
|
+
| Double-click | Enter group (edit children) |
|
|
222
|
+
| Escape | Exit group |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Canvas Elements
|
|
227
|
+
|
|
228
|
+
### Sticky Notes
|
|
229
|
+
|
|
230
|
+
Common in whiteboard apps.
|
|
231
|
+
|
|
232
|
+
```html
|
|
233
|
+
<div class="sticky-note" style="--sticky-color: #fef08a;">
|
|
234
|
+
<div class="sticky-note__content" contenteditable>
|
|
235
|
+
Type your idea here...
|
|
236
|
+
</div>
|
|
237
|
+
<div class="sticky-note__author">Alice</div>
|
|
238
|
+
</div>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```css
|
|
242
|
+
.sticky-note {
|
|
243
|
+
width: 200px;
|
|
244
|
+
min-height: 200px;
|
|
245
|
+
padding: 16px;
|
|
246
|
+
background: var(--sticky-color);
|
|
247
|
+
box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.15);
|
|
248
|
+
border-radius: 2px;
|
|
249
|
+
font-family: 'Comic Sans MS', cursive; /* Classic sticky feel */
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.sticky-note__content {
|
|
253
|
+
font-size: 16px;
|
|
254
|
+
line-height: 1.4;
|
|
255
|
+
outline: none;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.sticky-note__author {
|
|
259
|
+
position: absolute;
|
|
260
|
+
bottom: 8px;
|
|
261
|
+
right: 8px;
|
|
262
|
+
font-size: 11px;
|
|
263
|
+
opacity: 0.6;
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Sticky Note Colors:**
|
|
268
|
+
- Yellow (#fef08a) - General ideas
|
|
269
|
+
- Pink (#fbcfe8) - Questions
|
|
270
|
+
- Blue (#bfdbfe) - Insights
|
|
271
|
+
- Green (#bbf7d0) - Actions
|
|
272
|
+
- Orange (#fed7aa) - Concerns
|
|
273
|
+
|
|
274
|
+
### Connectors & Arrows
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
// Simple line connector
|
|
278
|
+
function drawConnector(from, to, ctx) {
|
|
279
|
+
// Calculate connection points (center of objects)
|
|
280
|
+
const start = { x: from.centerX, y: from.centerY };
|
|
281
|
+
const end = { x: to.centerX, y: to.centerY };
|
|
282
|
+
|
|
283
|
+
// Draw line
|
|
284
|
+
ctx.beginPath();
|
|
285
|
+
ctx.moveTo(start.x, start.y);
|
|
286
|
+
ctx.lineTo(end.x, end.y);
|
|
287
|
+
ctx.stroke();
|
|
288
|
+
|
|
289
|
+
// Draw arrowhead
|
|
290
|
+
const angle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
291
|
+
drawArrowhead(end, angle, ctx);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Shapes Library
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
┌─────────────────────────────────────────────────────┐
|
|
299
|
+
│ Shapes │
|
|
300
|
+
├─────────────────────────────────────────────────────┤
|
|
301
|
+
│ Basic │
|
|
302
|
+
│ ┌─┐ ○ ◇ △ ─ → ⬭ │
|
|
303
|
+
│ │
|
|
304
|
+
│ Flowchart │
|
|
305
|
+
│ ▭ ◇ ⬭ ▱ ⬡ │
|
|
306
|
+
│ │
|
|
307
|
+
│ Arrows │
|
|
308
|
+
│ → ⇒ ↔ ↕ ↗ │
|
|
309
|
+
└─────────────────────────────────────────────────────┘
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Performance Optimization
|
|
315
|
+
|
|
316
|
+
### Viewport Culling
|
|
317
|
+
|
|
318
|
+
Only render objects visible in the current viewport.
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
function getVisibleObjects(camera, objects) {
|
|
322
|
+
const viewport = getViewportBounds(camera);
|
|
323
|
+
|
|
324
|
+
return objects.filter(obj =>
|
|
325
|
+
intersects(obj.bounds, viewport)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function render() {
|
|
330
|
+
const visibleObjects = getVisibleObjects(camera, allObjects);
|
|
331
|
+
|
|
332
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
333
|
+
ctx.save();
|
|
334
|
+
|
|
335
|
+
// Apply camera transform
|
|
336
|
+
ctx.translate(camera.x, camera.y);
|
|
337
|
+
ctx.scale(camera.zoom, camera.zoom);
|
|
338
|
+
|
|
339
|
+
// Only render visible objects
|
|
340
|
+
visibleObjects.forEach(obj => obj.render(ctx));
|
|
341
|
+
|
|
342
|
+
ctx.restore();
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Level of Detail (LOD)
|
|
347
|
+
|
|
348
|
+
Simplify rendering at low zoom levels.
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
function renderObject(obj, ctx, zoom) {
|
|
352
|
+
if (zoom < 0.1) {
|
|
353
|
+
// Very zoomed out: just show bounding box
|
|
354
|
+
ctx.fillRect(obj.x, obj.y, obj.width, obj.height);
|
|
355
|
+
} else if (zoom < 0.5) {
|
|
356
|
+
// Moderately zoomed out: simplified render
|
|
357
|
+
obj.renderSimplified(ctx);
|
|
358
|
+
} else {
|
|
359
|
+
// Normal/zoomed in: full detail
|
|
360
|
+
obj.renderFull(ctx);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Performance Guidelines
|
|
366
|
+
|
|
367
|
+
| Technique | When to Apply |
|
|
368
|
+
|-----------|--------------|
|
|
369
|
+
| Viewport culling | Always for >100 objects |
|
|
370
|
+
| LOD rendering | Zoom levels <50% |
|
|
371
|
+
| Debounced render | Rapid camera movement |
|
|
372
|
+
| Canvas layering | Static vs. dynamic content |
|
|
373
|
+
| Web Workers | Heavy calculations |
|
|
374
|
+
| Virtual scrolling | Layer panel with 1000+ items |
|
|
375
|
+
|
|
376
|
+
### Canvas Size Limits
|
|
377
|
+
|
|
378
|
+
```
|
|
379
|
+
Practical limits:
|
|
380
|
+
- Browser canvas max: ~16,000 x 16,000 pixels
|
|
381
|
+
- Virtual canvas: Unlimited (render only visible portion)
|
|
382
|
+
- Object count: 10,000+ with proper culling
|
|
383
|
+
- Performance target: 60fps during pan/zoom
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Whiteboard-Specific Patterns
|
|
389
|
+
|
|
390
|
+
### Timer / Timeboxing
|
|
391
|
+
|
|
392
|
+
```html
|
|
393
|
+
<div class="timer">
|
|
394
|
+
<div class="timer__display">5:00</div>
|
|
395
|
+
<div class="timer__controls">
|
|
396
|
+
<button class="timer__btn--start">▶</button>
|
|
397
|
+
<button class="timer__btn--pause">⏸</button>
|
|
398
|
+
<button class="timer__btn--reset">↺</button>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Voting Sessions
|
|
404
|
+
|
|
405
|
+
```javascript
|
|
406
|
+
const votingSession = {
|
|
407
|
+
id: "vote-123",
|
|
408
|
+
topic: "Which design do we prefer?",
|
|
409
|
+
options: [
|
|
410
|
+
{ id: "a", label: "Option A", votes: [] },
|
|
411
|
+
{ id: "b", label: "Option B", votes: [] }
|
|
412
|
+
],
|
|
413
|
+
anonymous: true,
|
|
414
|
+
allowMultiple: false,
|
|
415
|
+
endTime: new Date("2024-12-20T14:00:00")
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
function castVote(sessionId, optionId, userId) {
|
|
419
|
+
const session = getSession(sessionId);
|
|
420
|
+
const vote = session.anonymous ? "anonymous" : userId;
|
|
421
|
+
|
|
422
|
+
session.options.find(o => o.id === optionId).votes.push(vote);
|
|
423
|
+
broadcastVoteUpdate(session);
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Facilitation Mode
|
|
428
|
+
|
|
429
|
+
Presenter controls for workshops.
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
┌─────────────────────────────────────────────────────┐
|
|
433
|
+
│ Facilitation Tools │
|
|
434
|
+
├─────────────────────────────────────────────────────┤
|
|
435
|
+
│ [Spotlight] [Timer] [Vote] [Lock Canvas] │
|
|
436
|
+
│ │
|
|
437
|
+
│ Participants: 12 active │
|
|
438
|
+
│ [Mute all annotations] [Bring everyone to me] │
|
|
439
|
+
└─────────────────────────────────────────────────────┘
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Clustering / Affinity Mapping
|
|
443
|
+
|
|
444
|
+
```javascript
|
|
445
|
+
// Auto-cluster similar sticky notes
|
|
446
|
+
function clusterStickies(stickies) {
|
|
447
|
+
const clusters = [];
|
|
448
|
+
|
|
449
|
+
stickies.forEach(sticky => {
|
|
450
|
+
// Find cluster by proximity or AI similarity
|
|
451
|
+
const nearbyCluster = findNearbyCluster(sticky, clusters);
|
|
452
|
+
|
|
453
|
+
if (nearbyCluster) {
|
|
454
|
+
nearbyCluster.add(sticky);
|
|
455
|
+
} else {
|
|
456
|
+
clusters.push(new Cluster([sticky]));
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
return clusters;
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Accessibility
|
|
467
|
+
|
|
468
|
+
### Keyboard Navigation
|
|
469
|
+
|
|
470
|
+
```javascript
|
|
471
|
+
// Full keyboard support for canvas
|
|
472
|
+
const keyboardControls = {
|
|
473
|
+
'Tab': () => selectNextObject(),
|
|
474
|
+
'Shift+Tab': () => selectPreviousObject(),
|
|
475
|
+
'Enter': () => enterEditMode(),
|
|
476
|
+
'Escape': () => exitEditMode(),
|
|
477
|
+
'Delete': () => deleteSelection(),
|
|
478
|
+
'ArrowUp': () => moveSelection(0, -1),
|
|
479
|
+
'ArrowDown': () => moveSelection(0, 1),
|
|
480
|
+
'ArrowLeft': () => moveSelection(-1, 0),
|
|
481
|
+
'ArrowRight': () => moveSelection(1, 0),
|
|
482
|
+
'Cmd+A': () => selectAll(),
|
|
483
|
+
'Cmd+Z': () => undo(),
|
|
484
|
+
'Cmd+Shift+Z': () => redo()
|
|
485
|
+
};
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Screen Reader Support
|
|
489
|
+
|
|
490
|
+
```html
|
|
491
|
+
<!-- Announce canvas state changes -->
|
|
492
|
+
<div role="application" aria-label="Design canvas">
|
|
493
|
+
<div aria-live="polite" class="sr-only" id="canvas-announcer">
|
|
494
|
+
<!-- Dynamic announcements -->
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div
|
|
498
|
+
role="img"
|
|
499
|
+
tabindex="0"
|
|
500
|
+
aria-describedby="canvas-description"
|
|
501
|
+
>
|
|
502
|
+
<canvas></canvas>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<script>
|
|
507
|
+
function announceSelection(objects) {
|
|
508
|
+
const announcer = document.getElementById('canvas-announcer');
|
|
509
|
+
announcer.textContent = `Selected ${objects.length} objects`;
|
|
510
|
+
}
|
|
511
|
+
</script>
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Drag and Drop Alternatives
|
|
515
|
+
|
|
516
|
+
Per WCAG 2.5.7, provide non-dragging alternatives:
|
|
517
|
+
|
|
518
|
+
```html
|
|
519
|
+
<!-- Context menu for move operations -->
|
|
520
|
+
<menu class="context-menu">
|
|
521
|
+
<button>Move to...</button>
|
|
522
|
+
<button>Bring to front</button>
|
|
523
|
+
<button>Send to back</button>
|
|
524
|
+
<button>Align left</button>
|
|
525
|
+
<button>Align center</button>
|
|
526
|
+
<button>Align right</button>
|
|
527
|
+
</menu>
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Key Metrics
|
|
533
|
+
|
|
534
|
+
| Metric | Value | Context |
|
|
535
|
+
|--------|-------|---------|
|
|
536
|
+
| Grid size | 4, 8, or 16px | Common options |
|
|
537
|
+
| Snap threshold | 2-8px | Distance before snapping |
|
|
538
|
+
| Undo stack | 100+ actions | Memory permitting |
|
|
539
|
+
| Object limit | 10,000+ | With culling |
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## Anti-Patterns
|
|
544
|
+
|
|
545
|
+
1. **No keyboard navigation** - Excludes users who can't use mouse
|
|
546
|
+
2. **Missing undo** - Critical for experimental exploration
|
|
547
|
+
3. **No snapping controls** - Users need to disable snapping sometimes
|
|
548
|
+
4. **Invisible layers** - Hard to find/select hidden objects
|
|
549
|
+
5. **No minimap** - Lost on large canvases
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Sources
|
|
554
|
+
|
|
555
|
+
- [Figma Engineering Blog](https://www.figma.com/blog/category/engineering/)
|
|
556
|
+
- [Miro - Infinite Canvas](https://miro.com/online-canvas-for-design/)
|
|
557
|
+
- [Konva.js Documentation](https://konvajs.org/)
|
|
558
|
+
- [LogRocket - Drag and Drop UI](https://blog.logrocket.com/ux-design/drag-and-drop-ui-examples/)
|
|
559
|
+
- [Cloudscape - Drag and Drop Patterns](https://cloudscape.design/patterns/general/drag-and-drop/)
|