@ctxr/skill-frontend-excellence 0.1.1

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.
@@ -0,0 +1,565 @@
1
+ # UI/UX Principles
2
+
3
+ Framework-agnostic patterns for interfaces that feel professional, predictable, and considered. Distilled from Apple Human Interface Guidelines, Material Design, NN/g research, and accumulated production practice.
4
+
5
+ ## The Hierarchy of UX Concerns
6
+
7
+ When a UI is "off" but you can't say why, walk this list top-down. The first thing that fails is what's wrong.
8
+
9
+ 1. **Can the user perceive it?** (Contrast, size, position, language.)
10
+ 2. **Can the user understand it?** (Vocabulary, hierarchy, affordance.)
11
+ 3. **Can the user operate it?** (Touch target, keyboard, focus, gesture, feedback.)
12
+ 4. **Does the user trust it?** (Consistency, predictability, recovery, transparency.)
13
+ 5. **Does it feel good?** (Polish, motion, sound, haptics, density.)
14
+
15
+ Steps 1-3 are correctness. Step 4 is reliability. Step 5 is craft.
16
+
17
+ ## State Coverage (the most-skipped quality bar)
18
+
19
+ Every screen and component must have intentional designs for all of these states:
20
+
21
+ | State | When | What to show |
22
+ |-------|------|--------------|
23
+ | Empty | No data yet | Specific, helpful message + primary action to fix it. Never a blank space. |
24
+ | Loading | Async fetch in progress | Skeleton screen for >300ms loads, spinner for shorter. Match the eventual layout. |
25
+ | Success | Action completed | Brief confirmation (toast, inline check, color flash). Move on quickly. |
26
+ | Error | Action failed | Cause + how to fix + recovery action. Never just "Something went wrong". |
27
+ | Partial | Some data, some loading | Render what you have, skeleton the rest. |
28
+ | Disabled | Action not available | Visually distinct (40-50% opacity), cursor not-allowed, programmatically disabled, optional explanation. |
29
+ | Read-only | Visible but not editable | Visually distinct from both editable AND disabled. |
30
+ | Stale | Data is older than fresh | Indicate freshness (timestamp, "updated 5m ago"), offer refresh. |
31
+ | Offline | No network | Disable network-dependent actions, show offline indicator, queue mutations if possible. |
32
+ | Unauthorized | User lacks permission | Explain why, link to upgrade or request access. Never silently disable. |
33
+ | Limit reached | Quota exhausted | Explain the limit, show progress, link to upgrade. |
34
+ | Initial / first-run | New user | Reduced surface, onboarding hints, sample data. |
35
+ | Successful state with no further action | Done | Brief celebration, clear next step. |
36
+
37
+ A new feature that ships with only the "happy path" state is half-built.
38
+
39
+ ### Cross-page consistency
40
+
41
+ State coverage is per-component, but components live on multiple pages. A component is not done when its states work on one page; it is done when the same states behave the same way on every page that hosts it. For shared widgets (cards, CTAs, headers, forms, modals), every state on every route must match the canonical contract.
42
+
43
+ When a state behaves differently on two routes, the cause is almost always the same: page-local CSS, duplicated markup, or a near-duplicate component. Fix the contract, not the page. Extraction discipline, the canonical contract, and drift detection live in [components.md](components.md). The cross-page audit procedure lives in [audit-workflow.md](audit-workflow.md).
44
+
45
+ ### Mobile drawer scroll lock
46
+
47
+ A mobile drawer (slide-out nav, bottom sheet, full-screen menu) must lock body scroll while open and restore the prior scroll position on close. Without lock, the underlying page scrolls behind the drawer and the user loses their place. Without restore, the user lands at the top after dismissing.
48
+
49
+ The lock is body-level: setting `overflow: hidden` on the document root is the standard pattern. The restore is captured at open time and reapplied via `window.scrollTo` on close. Drawer scroll lock failure is one of the standard checks in the geometry sweep; see [defects.md](defects.md).
50
+
51
+ ## Touch and Pointer Interaction
52
+
53
+ ### Hit targets
54
+
55
+ | Platform | Minimum |
56
+ |----------|--------|
57
+ | iOS / Web on touch | 44x44 CSS pixels (matches WCAG 2.5.5 Target Size Enhanced, Level AAA, from WCAG 2.1) |
58
+ | Android | 48x48 dp |
59
+ | Web on pointer | 24x24 CSS pixels minimum (WCAG 2.5.8 Target Size Minimum, Level AA, added in WCAG 2.2), 44x44 strongly recommended |
60
+
61
+ For icon-only buttons that look smaller, expand the hit area:
62
+
63
+ ```css
64
+ .icon-btn {
65
+ width: 24px;
66
+ height: 24px;
67
+ padding: 10px; /* total 44x44 hit area */
68
+ background-clip: content-box;
69
+ }
70
+ ```
71
+
72
+ ### Spacing between targets
73
+
74
+ Minimum 8 px gap between adjacent interactive targets. This prevents mistaps on touch and gives focus rings room to breathe.
75
+
76
+ ### Hover, press, focus, active
77
+
78
+ Every interactive element has four observable states:
79
+
80
+ | State | When | Visual change |
81
+ |-------|------|--------------|
82
+ | Resting | No interaction | The base style |
83
+ | Hover | Pointer over (not touch) | Subtle elevation, color shift, or underline |
84
+ | Focus | Keyboard or programmatic focus | Visible ring, 2-4px, contrast 3:1 against surface |
85
+ | Active / pressed | Mouse down or touch | Slight inset/scale (0.97 transform), darker background, depressed shadow |
86
+ | Disabled | Action unavailable | 40-50% opacity, cursor not-allowed, no hover/active response |
87
+
88
+ Touch devices skip hover. Don't put critical info in a hover-only tooltip.
89
+
90
+ ### Cursor
91
+
92
+ - `cursor: pointer` on every clickable element. Anti-pattern: `<button>` with default cursor.
93
+ - `cursor: text` on text inputs. Native, but don't override.
94
+ - `cursor: not-allowed` on disabled.
95
+ - `cursor: grab` / `grabbing` on draggables.
96
+ - `cursor: zoom-in` / `zoom-out` for image zoom.
97
+ - `cursor: help` for elements with `title` or tooltip.
98
+
99
+ ### Press feedback timing
100
+
101
+ Visual response within 80-150ms of press. Below 80ms feels disconnected (too instant). Above 200ms feels laggy.
102
+
103
+ ### Click vs tap vs hover
104
+
105
+ - **Click/tap**: primary actions only.
106
+ - **Hover**: never the only path to an action. Hover-reveal is fine for affordance but the action must also be reachable on touch (long-press, kebab menu, or always-visible).
107
+ - **Long-press**: secondary action on touch. Always provide a non-gesture alternative.
108
+ - **Right-click**: context menu only. Always provide an equivalent button or kebab menu.
109
+
110
+ ## Visual Hierarchy
111
+
112
+ Hierarchy is what tells the user where to look and in what order. Build it from these tools, in this priority:
113
+
114
+ 1. **Size**: bigger = more important.
115
+ 2. **Weight**: bolder = more important.
116
+ 3. **Contrast**: higher contrast against the background = more important.
117
+ 4. **Position**: top and left (in LTR) read first.
118
+ 5. **Color**: warm and saturated draws the eye; muted recedes.
119
+ 6. **Whitespace**: isolation makes things feel important.
120
+ 7. **Motion**: anything that moves draws attention.
121
+
122
+ Anti-patterns:
123
+
124
+ - Five things shouting at once. The eye doesn't know where to land.
125
+ - Equal weight on everything. Nothing is important.
126
+ - Color alone for hierarchy. Fails for colorblind users and in monochrome contexts.
127
+ - Borders everywhere. Borders compete with content.
128
+
129
+ ### The "primary action" rule
130
+
131
+ Each screen has exactly one primary CTA. Secondary actions are visually subordinate (outline, ghost, text-only). Tertiary actions are even quieter (link or icon only). Destructive actions are visually separated AND use a danger color but are not primary unless the screen is built for that destructive purpose.
132
+
133
+ ### F-pattern and Z-pattern
134
+
135
+ For sparse, hero-driven layouts: Z-pattern scan (top-left -> top-right -> diagonal -> bottom-right). Common placement: brand top-left, primary nav top-right, primary CTA bottom-right of the hero section.
136
+
137
+ For text-heavy layouts: F-pattern (left edge dominant scan). Important info on the leading edge. Sub-headings every 200-400px to provide scan anchors.
138
+
139
+ ## Density
140
+
141
+ Density is the ratio of information to space.
142
+
143
+ - **Low density**: hero-driven, single-focus surfaces. Generous whitespace, large type, one focus per section.
144
+ - **Medium density**: balanced application screens, mid-length content. Fits the majority of routine UI.
145
+ - **High density**: data tables, code editors, professional tools. Tight spacing, smaller type (still >= 14px), maximum information per pixel.
146
+
147
+ Match density to the user's task. A real-time trading terminal is high-density; a single-focus reading view is not. Mixing densities within the same surface feels wrong.
148
+
149
+ Rules:
150
+
151
+ - Within one density mode, be consistent.
152
+ - Provide a "compact / cozy / comfortable" toggle for data-heavy apps.
153
+ - Never sacrifice contrast or hit target for density.
154
+
155
+ ## Layout and Spacing
156
+
157
+ ### Spacing scale
158
+
159
+ Use a multiplicative scale, not arbitrary values. Common scales:
160
+
161
+ - **4-pt scale**: 0, 4, 8, 12, 16, 24, 32, 48, 64, 96 (Material Design).
162
+ - **8-pt scale**: 0, 8, 16, 24, 32, 48, 64 (a coarser variant).
163
+
164
+ Tailwind's default uses 4-pt. shadcn/ui uses 4-pt. Apple HIG uses 8-pt with halves at 4. Pick one and never deviate.
165
+
166
+ Anti-pattern: padding values like 7, 13, 17, 22. They feel random and break rhythm.
167
+
168
+ ### Component spacing tiers
169
+
170
+ | Tier | Value | Use |
171
+ |------|-------|-----|
172
+ | Inline | 4-8 | Inside a tag/badge/icon-text pair |
173
+ | Tight | 8-12 | Inside a button, input, or card |
174
+ | Comfortable | 16 | Between siblings inside a card |
175
+ | Section | 24-32 | Between cards or rows in a list |
176
+ | Block | 48-64 | Between major sections of a page |
177
+ | Page | 96-128 | Around the page edge on large viewports |
178
+
179
+ ### Whitespace as a tool
180
+
181
+ Whitespace groups related things and separates unrelated things. Use it intentionally, not residually.
182
+
183
+ The Gestalt law of proximity: things close together read as related. Items far apart read as separate. If two pieces of UI feel related but look unrelated, reduce the space. If two pieces feel unrelated but look unified, increase the space.
184
+
185
+ ## Typography in UI
186
+
187
+ (See [design.md](design.md) for the type aesthetic. This section is the functional rules.)
188
+
189
+ ### Sizes
190
+
191
+ | Use | Mobile | Desktop |
192
+ |-----|--------|---------|
193
+ | Body | 16px | 16px |
194
+ | Body small / caption | 14px | 14px |
195
+ | Body large / lead | 18-20px | 18-20px |
196
+ | H6 | 16px | 16px |
197
+ | H5 | 18px | 18-20px |
198
+ | H4 | 20-22px | 22-24px |
199
+ | H3 | 22-26px | 24-30px |
200
+ | H2 | 28-32px | 32-40px |
201
+ | H1 | 32-40px | 40-64px |
202
+ | Display (hero) | 40-56px | 56-96px |
203
+
204
+ Body text below 16px on mobile triggers iOS auto-zoom on focus (annoying). Avoid.
205
+
206
+ ### Line height
207
+
208
+ - Body: 1.5-1.75
209
+ - Headings: 1.1-1.3 (tighter)
210
+ - Buttons: 1 (single line) or 1.2
211
+
212
+ ### Line length
213
+
214
+ - Optimal: 60-75 characters per line.
215
+ - Mobile: 35-60.
216
+ - Desktop: cap at 75.
217
+ - Use `max-width: 65ch` (or similar) on long-form content.
218
+
219
+ ### Letter spacing
220
+
221
+ - Body: default.
222
+ - All-caps eyebrow text: +0.05em to +0.1em.
223
+ - Display headings: -0.01em to -0.02em (slight tighten).
224
+
225
+ ## Color in UI
226
+
227
+ (See [design.md](design.md) for palette construction. This section is the semantic system.)
228
+
229
+ ### Semantic tokens
230
+
231
+ Never use raw hex in components. Use semantic tokens:
232
+
233
+ | Token | Purpose |
234
+ |-------|--------|
235
+ | `--color-background` | Page background |
236
+ | `--color-surface` | Card / elevated surface |
237
+ | `--color-surface-muted` | Quieter surface (inset, secondary) |
238
+ | `--color-foreground` | Primary text |
239
+ | `--color-foreground-muted` | Secondary text |
240
+ | `--color-foreground-subtle` | Tertiary text, hints |
241
+ | `--color-border` | Resting border |
242
+ | `--color-border-strong` | Border on focus or emphasis |
243
+ | `--color-primary` | Brand action |
244
+ | `--color-primary-foreground` | Text on primary |
245
+ | `--color-success` | Positive state |
246
+ | `--color-warning` | Caution |
247
+ | `--color-danger` | Destructive / error |
248
+ | `--color-info` | Neutral information |
249
+ | `--color-focus-ring` | Focus indicator |
250
+
251
+ Map these tokens for both light and dark mode. Components reference tokens, never raw values.
252
+
253
+ ### Dark mode
254
+
255
+ - Don't invert. Design dark mode independently.
256
+ - Use desaturated, lighter tonal variants of the brand color.
257
+ - Surfaces in dark mode are typically `#0c0c10` or `#111` (not pure black). Pure black on OLED creates harsh contrast.
258
+ - Borders in dark mode are typically lighter than the surface (e.g., `rgba(255,255,255,0.08-0.12)`).
259
+ - Test contrast in dark mode separately. The 4.5:1 ratios are different.
260
+
261
+ ## Iconography
262
+
263
+ ### Source
264
+
265
+ - Use one icon set per product. Don't mix Heroicons + Lucide + Feather.
266
+ - SVG only. Never emoji as structural icons. Never PNG.
267
+ - Variable icons (Heroicons mini/outline/solid) are fine across hierarchy levels but consistent within a level.
268
+
269
+ ### Sizing
270
+
271
+ - 16px (xs): inline with body text or in tight UI.
272
+ - 20px (sm): default in buttons and inputs.
273
+ - 24px (md): nav items, section headers.
274
+ - 32px+ (lg): hero/feature illustrations.
275
+
276
+ ### Stroke width
277
+
278
+ - Outline icons: 1.5px or 2px stroke. Pick one and stay.
279
+ - Filled icons: no stroke.
280
+ - Don't mix outline and filled in the same hierarchy level.
281
+
282
+ ### Color
283
+
284
+ - Match the surrounding text or use a token (`--color-foreground-muted`).
285
+ - For status (success, warning, danger), use semantic color tokens.
286
+ - Always pair status icons with text or a label. Color alone is not enough.
287
+
288
+ ### Alignment
289
+
290
+ - Icons in buttons align to the text baseline.
291
+ - Icons in inputs align to the input's vertical center.
292
+ - Icons in lists align to the same x as adjacent rows.
293
+
294
+ ## Buttons
295
+
296
+ ### Hierarchy
297
+
298
+ | Level | Visual | Use |
299
+ |-------|--------|-----|
300
+ | Primary | Filled, brand color | One per screen. The primary action. |
301
+ | Secondary | Outlined or quiet filled | Alternative paths. |
302
+ | Tertiary | Text-only or ghost | Low-stakes actions. |
303
+ | Destructive primary | Filled, danger color | Only on screens explicitly built for the destructive action (delete confirmation). |
304
+ | Destructive secondary | Outlined, danger color | Listed alongside other actions. |
305
+ | Icon-only | Icon, no text | Compact UI. Always include `aria-label`. |
306
+
307
+ ### Anatomy
308
+
309
+ - Minimum 44x44 touch area.
310
+ - Padding: 12-16 horizontal, 8-12 vertical (small); 16-24 horizontal, 10-12 vertical (medium); 24-32 horizontal, 12-16 vertical (large).
311
+ - Border radius: tied to your style language. Pick a scale and use it.
312
+ - Icon spacing from text: 8 px.
313
+
314
+ ### States
315
+
316
+ - Resting, hover, focus, active, disabled, loading.
317
+ - Loading: replace icon with a spinner OR add a spinner before/after text. Disable the button. Maintain width to prevent layout shift.
318
+ - Disabled: 40-50% opacity, cursor not-allowed, programmatically disabled.
319
+
320
+ ## Forms (functional principles)
321
+
322
+ (See [forms.md](forms.md) for the deep dive.)
323
+
324
+ - Visible label for every input. Placeholder is not a label.
325
+ - Error inline, near the field, with cause + fix.
326
+ - Validate on blur, not on every keystroke.
327
+ - After submit error, focus the first invalid field.
328
+ - Mark required fields (asterisk + `aria-required="true"`).
329
+ - Use the right `type` and `autocomplete` attributes.
330
+ - Group related fields with `<fieldset>` + `<legend>`.
331
+ - Long forms autosave drafts.
332
+ - Multi-step forms show progress.
333
+
334
+ ## Modals and Overlays
335
+
336
+ ### Modal
337
+
338
+ - Reserved for blocking decisions (confirmation, login, critical data entry).
339
+ - Always escapable: Esc, backdrop click, close button.
340
+ - Focus trapped inside; restored on close.
341
+ - Background `inert` or `aria-hidden`.
342
+ - Backdrop scrim 40-60% black for legibility.
343
+ - Animate from trigger when possible (scale + fade).
344
+
345
+ ### Sheet / drawer
346
+
347
+ - Use for non-blocking secondary content (filters, details, settings).
348
+ - Slide from edge (left/right for navigation, bottom for mobile contextual).
349
+ - Same focus and escape rules as modal.
350
+ - Confirm before dismissing if there are unsaved changes.
351
+
352
+ ### Popover / tooltip
353
+
354
+ - Tooltip: hover-reveal of supplementary info. Plain text. No interactive content.
355
+ - Popover: click-reveal of richer content. Can contain interactive elements.
356
+ - Both must be keyboard-accessible and dismissible (Esc, click outside).
357
+
358
+ ### Toast / snackbar
359
+
360
+ - Brief notification of system events.
361
+ - Auto-dismiss in 3-5s for non-critical; persist for critical with explicit dismiss.
362
+ - Don't steal focus. Use `aria-live="polite"` (or `assertive` only for blocking errors).
363
+ - Maximum one or two on screen at a time. Stack newer below older or replace.
364
+
365
+ ## Navigation
366
+
367
+ ### Patterns
368
+
369
+ | Pattern | Use |
370
+ |---------|-----|
371
+ | Top app bar | Primary global navigation (web/desktop) |
372
+ | Bottom tab bar | Top-level navigation on mobile (max 5 items) |
373
+ | Side drawer / sidebar | Secondary nav, deep hierarchy, large screens |
374
+ | Hamburger menu | Mobile when bottom nav doesn't fit; not for primary on desktop |
375
+ | Breadcrumbs | Hierarchy 3+ levels deep |
376
+ | Tabs | Switching between related views of the same context |
377
+ | Segmented control | Two to four mutually exclusive options |
378
+ | Stepper / wizard | Multi-step linear flows |
379
+
380
+ ### Rules
381
+
382
+ - Show the user where they are. Active tab/route highlighted.
383
+ - Predictable back: browser back, swipe-back (iOS), system back (Android) all do the same thing.
384
+ - Preserve scroll and state on back navigation.
385
+ - Deep linking: every screen has a URL. Sharing a link works.
386
+ - Don't mix navigation patterns at the same hierarchy level (don't have tabs + sidebar + bottom nav all primary).
387
+ - Persistent: core nav reachable from deep pages.
388
+ - Adaptive: large screens prefer sidebar; small screens use bottom/top.
389
+ - Destructive actions (delete account, sign out) visually separated from regular nav.
390
+
391
+ ## Loading and Async
392
+
393
+ ### Show feedback within 100ms
394
+
395
+ Any user action that triggers async work needs feedback within 100ms or the user assumes nothing happened. Visual: button state change, immediate spinner, optimistic UI.
396
+
397
+ ### Skeleton screens vs spinners
398
+
399
+ | Duration | Pattern |
400
+ |----------|---------|
401
+ | < 300ms | Nothing. The result will appear. |
402
+ | 300ms - 1s | Spinner or progress indicator. |
403
+ | 1s+ | Skeleton screen matching the eventual layout. |
404
+ | 5s+ | Progress indicator (percent or step count) + estimated remaining. |
405
+ | 10s+ | Allow user to leave or background the operation. |
406
+
407
+ ### Optimistic UI
408
+
409
+ For high-confidence operations (like a heart, save a draft), update the UI immediately and reconcile on response. Roll back on error with a clear message.
410
+
411
+ ### Rate limiting and debouncing
412
+
413
+ - Search input: debounce 200-300ms.
414
+ - Auto-save: debounce 500-1000ms.
415
+ - Window resize: throttle 100-200ms.
416
+ - Scroll: throttle 16ms (one frame) for animation, longer for analytics.
417
+
418
+ ## Empty States
419
+
420
+ The empty state is content. Treat it as a designed surface.
421
+
422
+ - **Specific message.** "No items yet" beats "No data".
423
+ - **Why it's empty.** State the condition that fills it: "Items appear here once you create one."
424
+ - **Primary action.** Offer the action that resolves the empty state, e.g., "Create item".
425
+ - **Optional illustration.** Match the brand. Don't use a generic stock illustration.
426
+
427
+ Anti-patterns:
428
+
429
+ - Blank space.
430
+ - "No results found" with no follow-up.
431
+ - A hopeful illustration with no action.
432
+
433
+ ## Error Recovery
434
+
435
+ - Cause AND fix, in plain language. "Email address is required" beats "Validation error". "Couldn't connect to server, check your network and try again" beats "Network error".
436
+ - Provide the recovery action (retry button, edit field, contact support link).
437
+ - Don't blame the user. "We couldn't save your draft" not "You didn't save".
438
+ - Preserve user input on error. Never make them retype.
439
+ - For 404 / 500: brand-consistent design, search box if applicable, link home, link to support, optionally a delightful detail (don't overdo).
440
+
441
+ ## Notifications and Badges
442
+
443
+ - Use sparingly. Notification fatigue is real.
444
+ - Numeric badges for unread/pending counts. Cap at 99+ for sanity.
445
+ - Dot badges for "something new" without count.
446
+ - Clear notifications when the user has acknowledged them.
447
+ - Group similar notifications (5 likes from 5 people = "5 people liked your post").
448
+ - Provide a notification preferences page.
449
+
450
+ ## Search
451
+
452
+ - Search box prominent: top of page or in the global header.
453
+ - Show recent searches when empty.
454
+ - Show suggestions while typing (debounced).
455
+ - Show results in real-time or with a clear submit button.
456
+ - Empty result state: "No results for [query]" + spelling check + alternate suggestions.
457
+ - Search filters: visible, removable individually, with a "clear all".
458
+ - Recent and saved searches.
459
+
460
+ ## Data Tables
461
+
462
+ - Sortable columns indicate current sort with an arrow icon and `aria-sort`.
463
+ - Sticky header on long tables.
464
+ - Row selection with checkboxes (with select-all in header).
465
+ - Bulk actions appear when one or more rows selected.
466
+ - Inline actions on hover (desktop) or always-visible (touch).
467
+ - Pagination or infinite scroll, never both.
468
+ - Empty state when no rows.
469
+ - Loading skeleton matching the table structure.
470
+
471
+ ## Settings and Preferences
472
+
473
+ - Group related settings.
474
+ - Use the right control: toggle for boolean, radio for one-of-N, checkbox for any-of-N, dropdown for many-of-N, slider for continuous.
475
+ - Apply on change, not on submit. (Confirm only if the change is destructive.)
476
+ - Show the saved state ("Saved 2s ago" microcopy).
477
+ - Reset to default per setting; never a global "reset all".
478
+
479
+ ## Toolbars and Action Bars
480
+
481
+ - Group actions by relationship.
482
+ - Show only top 3-5 actions; overflow into kebab menu.
483
+ - Destructive actions in the overflow menu unless they're the primary action of the screen.
484
+ - Tooltip on icon-only actions.
485
+
486
+ ## Onboarding
487
+
488
+ - Don't onboarding-dump. Show value first, teach as needed.
489
+ - Use empty states to teach (one feature per empty state).
490
+ - Use coachmarks sparingly: only for novel patterns the user can't discover.
491
+ - Skippable. Always.
492
+ - Resumable. The user can return to onboarding from settings.
493
+
494
+ ## Sample Data and First-Run Experience
495
+
496
+ For surfaces that come alive only after the user contributes data (data tools, editors, analytics views):
497
+
498
+ - Provide a sample dataset or demo workspace so the user sees the surface working before contributing real data.
499
+ - Make it easy to switch between sample and real data.
500
+ - The surface should be useful without 30 minutes of setup.
501
+
502
+ ## Microcopy
503
+
504
+ The text in the UI is part of the design.
505
+
506
+ - **Concrete, not abstract.** "Save draft" not "Save". "Delete user" not "OK".
507
+ - **Active voice.** "Choose a plan" not "A plan must be chosen".
508
+ - **Present tense.** "Settings saved" not "Settings have been saved".
509
+ - **No jargon for end users.** Write to the user's vocabulary.
510
+ - **Consistent.** "Sign in" everywhere, not "Log in" / "Sign in" / "Login" mixed.
511
+ - **Honest.** "We can't process this card" not "Something went wrong".
512
+ - **Brief.** Cut adjectives. "Saving" not "Now saving your changes".
513
+
514
+ ## Localization
515
+
516
+ - Don't bake English-specific assumptions into layouts. German is 30% longer; Japanese can be 60% shorter.
517
+ - Use logical CSS properties (`margin-inline-start`, `padding-block`) for RTL support.
518
+ - Date, time, number, currency: format via `Intl.*` APIs, not custom code.
519
+ - Pluralization: use ICU MessageFormat or the framework equivalent.
520
+ - Translatable strings extracted to a single source. Never inline string concatenation for translatable text.
521
+
522
+ ## Common UX Mistakes
523
+
524
+ - Hover-only actions on touch devices.
525
+ - Modals without focus traps or Esc to close.
526
+ - Tooltips with critical information.
527
+ - Required fields not marked.
528
+ - Error messages without a fix.
529
+ - Loading states that disappear before the user perceives them.
530
+ - Empty states that are blank.
531
+ - Disabled buttons that don't say why.
532
+ - Confirmation dialogs for non-destructive actions.
533
+ - No confirmation for destructive actions.
534
+ - Auto-advancing carousels with no pause.
535
+ - Carousels at all. (They have low engagement; static heroes outperform.)
536
+ - Modal interrupting the user's task.
537
+ - Toast that disappears before the user can read it.
538
+ - Hamburger menu on a desktop hero where horizontal space is plentiful.
539
+ - Search results that change layout shape on every query.
540
+
541
+ ## Self-Healing for UX
542
+
543
+ Before declaring work complete:
544
+
545
+ - [ ] Every screen has empty, loading, success, error, and disabled state designs
546
+ - [ ] Every interactive element has hover, focus, active, disabled states with visible focus
547
+ - [ ] Touch targets >= 44x44 with 8px gaps
548
+ - [ ] One primary CTA per screen
549
+ - [ ] Visual hierarchy: scan the page; the most important thing is the largest/boldest
550
+ - [ ] Density matches the user's job
551
+ - [ ] Spacing follows the chosen scale; no random values
552
+ - [ ] Iconography: one set, one stroke width, consistent sizes
553
+ - [ ] Modals: focus trapped, Esc closes, focus restored
554
+ - [ ] Forms: labeled, validated on blur, error inline + accessible
555
+ - [ ] Navigation: where am I, how do I get back, all key screens deep-linkable
556
+ - [ ] Loading: feedback within 100ms, skeleton at 300ms
557
+ - [ ] Microcopy: concrete, active, present tense, no jargon
558
+ - [ ] Tested on smallest target viewport, 200% zoom, dark mode, reduced motion, keyboard only
559
+
560
+ ## See Also
561
+
562
+ - [design.md](design.md) for aesthetic and brand expression
563
+ - [forms.md](forms.md) for form-specific patterns
564
+ - [motion.md](motion.md) for state transitions
565
+ - [data-viz.md](data-viz.md) for tables and charts
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // Fails when tracked Markdown files contain U+2014 (em) or U+2013 (en) characters.
3
+ // Walks SKILL.md, README.md, CONTRIBUTING.md, CHANGELOG.md, and references/**/*.md.
4
+ // Skips fenced code blocks, this script itself, node_modules, and .git.
5
+ // ESM, stdlib only. Run: node scripts/check-no-dashes.mjs
6
+ import { readFile, readdir, stat } from 'node:fs/promises';
7
+ import { join, relative, dirname, sep } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import process from 'node:process';
10
+
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+ const EM = '—';
13
+ const EN = '–';
14
+
15
+ async function findRoot(start) {
16
+ let dir = start;
17
+ // Walk up looking for package.json (preferred) or .git (fallback for skill repos).
18
+ for (;;) {
19
+ for (const marker of ['package.json', '.git']) {
20
+ try {
21
+ await stat(join(dir, marker));
22
+ return dir;
23
+ } catch { /* keep searching */ }
24
+ }
25
+ const parent = dirname(dir);
26
+ if (parent === dir) throw new Error('repo root not found');
27
+ dir = parent;
28
+ }
29
+ }
30
+
31
+ async function* walk(dir) {
32
+ let entries;
33
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
34
+ for (const e of entries) {
35
+ if (e.name === 'node_modules' || e.name === '.git') continue;
36
+ const full = join(dir, e.name);
37
+ if (e.isDirectory()) yield* walk(full);
38
+ else if (e.isFile() && e.name.endsWith('.md')) yield full;
39
+ }
40
+ }
41
+
42
+ async function collectFiles(root) {
43
+ const out = [];
44
+ for (const top of ['SKILL.md', 'README.md', 'CONTRIBUTING.md', 'CHANGELOG.md']) {
45
+ try { await stat(join(root, top)); out.push(join(root, top)); } catch { /* missing is fine */ }
46
+ }
47
+ for await (const p of walk(join(root, 'references'))) out.push(p);
48
+ return out;
49
+ }
50
+
51
+ function scan(text) {
52
+ const hits = [];
53
+ let inFence = false;
54
+ const lines = text.split('\n');
55
+ for (let i = 0; i < lines.length; i++) {
56
+ const line = lines[i];
57
+ // Toggle on any line whose first non-space content is a triple backtick fence.
58
+ if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
59
+ if (inFence) continue;
60
+ for (let c = 0; c < line.length; c++) {
61
+ const ch = line[c];
62
+ if (ch === EM) hits.push({ line: i + 1, col: c + 1, kind: 'EM' });
63
+ else if (ch === EN) hits.push({ line: i + 1, col: c + 1, kind: 'EN' });
64
+ }
65
+ }
66
+ return hits;
67
+ }
68
+
69
+ async function main() {
70
+ const root = await findRoot(HERE);
71
+ const selfRel = relative(root, fileURLToPath(import.meta.url));
72
+ const files = (await collectFiles(root)).filter(f => relative(root, f) !== selfRel);
73
+ const results = await Promise.all(files.map(async f => ({ file: f, hits: scan(await readFile(f, 'utf8')) })));
74
+ let totalHits = 0, filesWithHits = 0;
75
+ for (const r of results) {
76
+ if (!r.hits.length) continue;
77
+ filesWithHits++;
78
+ totalHits += r.hits.length;
79
+ const rel = relative(root, r.file).split(sep).join('/');
80
+ for (const h of r.hits) process.stderr.write(`${rel}:${h.line}:${h.col}: ${h.kind} dash\n`);
81
+ }
82
+ if (totalHits === 0) {
83
+ process.stdout.write(`check-no-dashes: OK (${files.length} files scanned)\n`);
84
+ process.exit(0);
85
+ }
86
+ process.stderr.write(`check-no-dashes: FAIL (${totalHits} hits in ${filesWithHits} files)\n`);
87
+ process.exit(1);
88
+ }
89
+
90
+ main().catch(err => { process.stderr.write(`check-no-dashes: ERROR ${err.message}\n`); process.exit(2); });