@maccesar/aiskills 1.11.0 → 1.15.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.
Files changed (60) hide show
  1. package/README.md +43 -8
  2. package/lib/cleanup.js +42 -0
  3. package/lib/commands/skills.js +110 -8
  4. package/lib/config.js +17 -12
  5. package/lib/installer.js +5 -3
  6. package/lib/platform.js +1 -1
  7. package/lib/symlink.js +45 -3
  8. package/lib/utils.js +41 -0
  9. package/package.json +1 -1
  10. package/skills/audit-codebase/SKILL.md +70 -0
  11. package/skills/audit-codebase/agents/openai.yaml +4 -0
  12. package/skills/audit-codebase/references/comprehensive-audit.md +220 -0
  13. package/skills/audit-codebase/references/report-format.md +119 -0
  14. package/skills/humaniza/SKILL.md +55 -4
  15. package/skills/humaniza/references/ai-patterns-es.md +40 -0
  16. package/skills/humaniza/references/checklist.md +9 -0
  17. package/skills/humaniza/references/examples.md +16 -0
  18. package/skills/humaniza/references/lexicon-es-mx.md +18 -0
  19. package/skills/humaniza/references/structures-es.md +132 -0
  20. package/skills/humaniza/scripts/check_ai_patterns.py +216 -0
  21. package/skills/refactoring-ui/SKILL.md +65 -29
  22. package/skills/refactoring-ui/references/05-motion.md +124 -0
  23. package/skills/refactoring-ui/references/06-dark-mode.md +117 -0
  24. package/skills/refactoring-ui/references/07-component-patterns.md +181 -0
  25. package/skills/stitch-showcase/SKILL.md +24 -232
  26. package/skills/stitch-showcase/references/07-theme-system.md +12 -0
  27. package/skills/stitch-showcase/references/08-type-detection.md +9 -1
  28. package/skills/stitch-showcase/references/10-component-standardization.md +25 -0
  29. package/skills/stitch-showcase/references/12-video-embedding.md +113 -0
  30. package/skills/stitch-showcase/references/13-language-detection.md +82 -0
  31. package/skills/stitch-showcase/references/14-troubleshooting-known-issues.md +122 -0
  32. package/skills/stitch-showcase/references/15-build-flags.md +71 -0
  33. package/skills/stitch-showcase/references/16-design-md-format.md +107 -0
  34. package/skills/stitch-showcase/references/index.html +25 -19
  35. package/skills/stitch-showcase/references/viewer.html +24 -12
  36. package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
  37. package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-314.pyc +0 -0
  38. package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-314.pyc +0 -0
  39. package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-314.pyc +0 -0
  40. package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-314.pyc +0 -0
  41. package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-314.pyc +0 -0
  42. package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-314.pyc +0 -0
  43. package/skills/stitch-showcase/scripts/__pycache__/slug_demangle.cpython-314.pyc +0 -0
  44. package/skills/stitch-showcase/scripts/build_showcase.py +150 -10
  45. package/skills/stitch-showcase/scripts/parse_design_md.py +145 -12
  46. package/skills/stitch-showcase/scripts/slug_demangle.py +209 -0
  47. package/skills/vscode-extension-dev/SKILL.md +90 -41
  48. package/skills/vscode-extension-dev/references/api-additional.md +168 -0
  49. package/skills/vscode-extension-dev/references/api-progress.md +55 -0
  50. package/skills/vscode-extension-dev/references/api-quickpick.md +75 -0
  51. package/skills/vscode-extension-dev/references/api-secretstorage.md +57 -0
  52. package/skills/vscode-extension-dev/references/api-statusbar.md +38 -0
  53. package/skills/vscode-extension-dev/references/api-treeview.md +78 -0
  54. package/skills/vscode-extension-dev/references/api-webview.md +149 -0
  55. package/skills/vscode-extension-dev/references/architecture.md +67 -0
  56. package/skills/vscode-extension-dev/references/debugger.md +179 -0
  57. package/skills/vscode-extension-dev/references/lsp.md +175 -0
  58. package/skills/vscode-extension-dev/references/notebooks.md +208 -0
  59. package/skills/vscode-extension-dev/references/testing.md +208 -0
  60. package/skills/vscode-extension-dev/references/api-patterns.md +0 -625
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ check_ai_patterns.py — Deterministic scanner for AI-writing tics in Spanish.
4
+
5
+ Reads patterns from sibling `references/lexicon-es-mx.md` and reports every
6
+ hit found in the input text with line number, category, matched phrase, and
7
+ a replacement suggestion when the lexicon supplies one.
8
+
9
+ Usage:
10
+ python check_ai_patterns.py <text_file>
11
+ python check_ai_patterns.py < input.txt
12
+ cat input.txt | python check_ai_patterns.py
13
+
14
+ Stdlib only. Python 3.8+.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ import sys
21
+ from pathlib import Path
22
+ from typing import Iterator, NamedTuple
23
+
24
+
25
+ SCRIPT_DIR = Path(__file__).resolve().parent
26
+ LEXICON_PATH = SCRIPT_DIR.parent / "references" / "lexicon-es-mx.md"
27
+
28
+
29
+ class Hit(NamedTuple):
30
+ line_no: int
31
+ column: int
32
+ category: str
33
+ matched: str
34
+ suggestion: str # empty string if none
35
+
36
+
37
+ def _parse_section_header(line: str) -> str:
38
+ """Return the header text if line is `## ...`, else empty string."""
39
+ if line.startswith("## "):
40
+ return line[3:].strip().rstrip(":")
41
+ return ""
42
+
43
+
44
+ def _split_phrases(content: str) -> list[str]:
45
+ """
46
+ Split a comma-separated phrase block into individual phrases.
47
+ Strips whitespace and a trailing period.
48
+ """
49
+ cleaned = content.strip().rstrip(".")
50
+ return [p.strip() for p in cleaned.split(",") if p.strip()]
51
+
52
+
53
+ def _parse_replacement_line(line: str) -> tuple[str, str] | None:
54
+ """
55
+ Parse a lexicon line of the form `- phrase -> suggestion` or
56
+ `phrase -> suggestion`. Returns (phrase, suggestion) or None.
57
+ """
58
+ stripped = line.lstrip("-").strip()
59
+ if "->" not in stripped:
60
+ return None
61
+ left, _, right = stripped.partition("->")
62
+ phrase = left.strip()
63
+ suggestion = right.strip()
64
+ # Strip any inline parenthetical hint on the phrase side, e.g. `aplicar a (trabajo)`.
65
+ # We keep the visible form so the user can locate it, but build a search regex
66
+ # that ignores the hint. Here, we keep the phrase as-is and let the search
67
+ # tolerate the parenthetical by stripping it for matching only.
68
+ return phrase, suggestion
69
+
70
+
71
+ def load_lexicon(path: Path) -> dict[str, list[tuple[str, str]]]:
72
+ """
73
+ Parse the lexicon markdown file into a dict of:
74
+ category -> list of (phrase, suggestion) tuples.
75
+
76
+ Suggestion is an empty string when the lexicon offers none.
77
+ """
78
+ if not path.exists():
79
+ return {}
80
+
81
+ categories: dict[str, list[tuple[str, str]]] = {}
82
+ current_category = ""
83
+ current_inline_block: list[str] = []
84
+
85
+ def flush_inline_block() -> None:
86
+ if not current_category or not current_inline_block:
87
+ return
88
+ text = " ".join(current_inline_block)
89
+ phrases = _split_phrases(text)
90
+ for p in phrases:
91
+ categories.setdefault(current_category, []).append((p, ""))
92
+ current_inline_block.clear()
93
+
94
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
95
+ line = raw_line.rstrip()
96
+
97
+ header = _parse_section_header(line)
98
+ if header:
99
+ flush_inline_block()
100
+ current_category = header
101
+ continue
102
+
103
+ if not current_category:
104
+ continue
105
+
106
+ if not line.strip():
107
+ flush_inline_block()
108
+ continue
109
+
110
+ replacement = _parse_replacement_line(line)
111
+ if replacement is not None:
112
+ phrase, suggestion = replacement
113
+ categories.setdefault(current_category, []).append((phrase, suggestion))
114
+ continue
115
+
116
+ if line.startswith("-"):
117
+ # Bullet item without `->`: treat as a plain phrase.
118
+ phrase = line.lstrip("-").strip()
119
+ if phrase:
120
+ categories.setdefault(current_category, []).append((phrase, ""))
121
+ continue
122
+
123
+ # Otherwise, this is part of an inline comma-separated block.
124
+ current_inline_block.append(line)
125
+
126
+ flush_inline_block()
127
+ return categories
128
+
129
+
130
+ def _compile_pattern(phrase: str) -> re.Pattern[str]:
131
+ """
132
+ Build a case-insensitive regex matching the phrase as a whole-word boundary,
133
+ tolerating an optional parenthetical hint (e.g. `aplicar a (trabajo)` matches
134
+ `aplicar a`).
135
+ """
136
+ bare = re.sub(r"\s*\([^)]*\)\s*", "", phrase).strip()
137
+ escaped = re.escape(bare)
138
+ # Allow optional whitespace inside multi-word phrases.
139
+ escaped = escaped.replace(r"\ ", r"\s+")
140
+ return re.compile(rf"(?<!\w){escaped}(?!\w)", re.IGNORECASE)
141
+
142
+
143
+ def scan_text(
144
+ text: str,
145
+ categories: dict[str, list[tuple[str, str]]],
146
+ ) -> Iterator[Hit]:
147
+ """Yield a Hit for every phrase match found in the text."""
148
+ compiled = [
149
+ (category, phrase, suggestion, _compile_pattern(phrase))
150
+ for category, items in categories.items()
151
+ for phrase, suggestion in items
152
+ ]
153
+
154
+ for line_no, line in enumerate(text.splitlines(), start=1):
155
+ for category, phrase, suggestion, pattern in compiled:
156
+ for match in pattern.finditer(line):
157
+ yield Hit(
158
+ line_no=line_no,
159
+ column=match.start() + 1,
160
+ category=category,
161
+ matched=match.group(0),
162
+ suggestion=suggestion,
163
+ )
164
+
165
+
166
+ def read_input() -> str:
167
+ if len(sys.argv) > 1:
168
+ arg = sys.argv[1]
169
+ if arg in ("-h", "--help"):
170
+ print(__doc__)
171
+ sys.exit(0)
172
+ return Path(arg).read_text(encoding="utf-8")
173
+ return sys.stdin.read()
174
+
175
+
176
+ def format_hit(hit: Hit) -> str:
177
+ location = f"line {hit.line_no}:{hit.column}"
178
+ body = f'[{hit.category}] "{hit.matched}"'
179
+ if hit.suggestion:
180
+ body += f" → {hit.suggestion}"
181
+ return f"{location} {body}"
182
+
183
+
184
+ def main() -> int:
185
+ categories = load_lexicon(LEXICON_PATH)
186
+ if not categories:
187
+ print(
188
+ f"WARNING: lexicon not found or empty at {LEXICON_PATH}",
189
+ file=sys.stderr,
190
+ )
191
+ return 2
192
+
193
+ text = read_input()
194
+ hits = list(scan_text(text, categories))
195
+
196
+ if not hits:
197
+ print("OK: no AI-writing tics detected.")
198
+ return 0
199
+
200
+ print(f"Found {len(hits)} potential AI-writing tic(s):\n")
201
+ for hit in hits:
202
+ print(format_hit(hit))
203
+
204
+ by_category: dict[str, int] = {}
205
+ for hit in hits:
206
+ by_category[hit.category] = by_category.get(hit.category, 0) + 1
207
+
208
+ print("\nSummary by category:")
209
+ for category, count in sorted(by_category.items(), key=lambda x: -x[1]):
210
+ print(f" {count:>3} {category}")
211
+
212
+ return 1
213
+
214
+
215
+ if __name__ == "__main__":
216
+ sys.exit(main())
@@ -1,32 +1,65 @@
1
1
  ---
2
2
  name: refactoring-ui
3
- description: Design advisor for UI/UX work, drawing on principles from "Refactoring UI" by Adam Wathan & Steve Schoger. Use when the user asks for UI/UX design advice, design reviews, visual hierarchy improvements, color system help, typography guidance, spacing decisions, depth/shadow usage, image handling, or finishing touches on any interface.
4
- when_to_use: >
5
- - User asks "how do I make this look better?"
6
- - User asks about color palettes, type scales, or spacing systems
7
- - User asks about visual hierarchy or emphasis
8
- - User is designing a UI component, page, or layout
9
- - User wants a design review or critique
10
- - User asks about shadows, depth, or layering
11
- - User asks about handling images in UI
12
- - User asks about empty states, borders, or decorative elements
13
- source: "Inspired by 'Refactoring UI' by Adam Wathan & Steve Schoger — refactoringui.com"
14
- anti_hallucination_note: >
15
- Recommendations in this skill summarize design principles in the maintainer's own words.
16
- Do NOT invent ratios, scale values, or technical numbers that are not in the reference
17
- files. If a topic is not covered, say so explicitly rather than inventing advice.
3
+ description: 'Use when the user asks for UI/UX design advice, design reviews, visual hierarchy improvements, color system help, typography guidance, spacing decisions, depth/shadow usage, image handling, or finishing touches on any interface. Inspired by "Refactoring UI" by Adam Wathan & Steve Schoger. Triggers: "how do I make this look better?", color palettes, type scales, spacing systems, visual hierarchy, design reviews, shadows/depth, image handling, empty states, borders.'
18
4
  ---
19
5
 
20
6
  # Refactoring UI Skill
21
7
 
22
8
  Design advice grounded in the principles taught by Adam Wathan & Steve Schoger in their book *Refactoring UI*. **The book itself contains the original prose, illustrations, side-by-side examples, and case studies — buy it at https://refactoringui.com for the full material.** This skill provides reformulated guidance for use as an AI assistant reference.
23
9
 
24
- ## How to Use This Skill
10
+ ## Required workflow (read before responding)
25
11
 
26
- 1. Read the relevant reference file(s) before answering
27
- 2. Base advice on the reference content not training data or unrelated design systems
28
- 3. Speak in your own words; do not reproduce the book's prose, illustrations, or examples verbatim
29
- 4. Do not invent numbers, ratios, or specific technical rules that are not in the references
12
+ The SKILL.md alone is an **index** of references. The detail you need
13
+ to give accurate answers lives in the reference files. **Reading this
14
+ SKILL.md is not enough.**
15
+
16
+ ### Step 1 — Open the relevant reference files
17
+
18
+ | Task involves | Required reading |
19
+ |---|---|
20
+ | Color systems, typography, spacing scales — defining the system | [references/01-foundations.md](references/01-foundations.md) |
21
+ | Layout, white space, visual hierarchy, page-level structure | [references/02-page-mechanics.md](references/02-page-mechanics.md) |
22
+ | Color usage, HSL, greys, contrast, shadows, depth, images | [references/03-visual-treatment.md](references/03-visual-treatment.md) |
23
+ | Empty states, borders, accents, finishing touches | [references/04-polish.md](references/04-polish.md) |
24
+ | Motion, microinteractions, transitions, hover/press states, loading | [references/05-motion.md](references/05-motion.md) |
25
+ | Dark mode, multi-theme color tokens, theme toggle, contrast strategy | [references/06-dark-mode.md](references/06-dark-mode.md) |
26
+ | Modals, forms, tables — component-specific layout and behavior patterns | [references/07-component-patterns.md](references/07-component-patterns.md) |
27
+
28
+ ### Step 2 — Output contract
29
+
30
+ Every design recommendation, ratio, value, or rule you cite MUST be
31
+ backed by a citation in the form:
32
+
33
+ `[source: references/<file>.md]`
34
+
35
+ Example: *"Use weight and color, not just font size, to establish hierarchy [source: references/02-page-mechanics.md]"*
36
+
37
+ ### Step 3 — If you must answer from memory
38
+
39
+ If you write a claim without having read the reference that backs it,
40
+ prepend `FROM_MEMORY (unverified):` to that claim. Do not hide it.
41
+
42
+ ### Banned behaviors
43
+
44
+ - ❌ Inventing ratios, scale values, contrast numbers, or rules not in the references
45
+ - ❌ Reproducing the book's prose, illustrations, or examples verbatim — paraphrase only
46
+ - ❌ Mixing in advice from unrelated design systems (Material, HIG, Tailwind defaults) as if it were *Refactoring UI* doctrine
47
+ - ❌ Marking the answer complete without listing which reference files you read
48
+
49
+ ## When to use
50
+
51
+ - User asks "how do I make this look better?"
52
+ - User asks about color palettes, type scales, or spacing systems
53
+ - User asks about visual hierarchy or emphasis
54
+ - User is designing a UI component, page, or layout
55
+ - User wants a design review or critique
56
+ - User asks about shadows, depth, or layering
57
+ - User asks about handling images in UI
58
+ - User asks about empty states, borders, or decorative elements
59
+
60
+ ## Source
61
+
62
+ Inspired by 'Refactoring UI' by Adam Wathan & Steve Schoger — refactoringui.com
30
63
 
31
64
  ## Reference Files
32
65
 
@@ -36,18 +69,21 @@ Design advice grounded in the principles taught by Adam Wathan & Steve Schoger i
36
69
  | `references/02-page-mechanics.md` | Visual hierarchy, layout, white space, spacing scales, typography |
37
70
  | `references/03-visual-treatment.md` | Color systems (HSL, shades, greys, contrast), depth and shadows, image handling |
38
71
  | `references/04-polish.md` | Finishing touches: borders, accents, empty states, decorative defaults, design intuition |
72
+ | `references/05-motion.md` | Motion system (durations, easings), hover/press states, loading patterns, prefers-reduced-motion — **complementary** (not from RUI) |
73
+ | `references/06-dark-mode.md` | Dark mode color tokens, text contrast, shadow handling, images, theme toggle — **complementary** (extrapolates RUI's HSL principles) |
74
+ | `references/07-component-patterns.md` | Modals (focus, layout), forms (labels, validation), tables (density, alignment) — **complementary** (extends RUI principles to specific components) |
39
75
 
40
76
  ## Anti-Patterns to Watch For
41
77
 
42
- - Designing layouts/navs/shells before designing real features
43
- - Using font size as the only tool for hierarchy (ignoring weight and color)
44
- - Using opacity to create grey text on colored backgrounds
45
- - Starting with too little white space and adding it later
46
- - Using `em` for type scales (compounds when nested)
47
- - Using color as the only signal for a UI state
48
- - Designing with placeholder images instead of real content
49
- - Shrinking a logo down to use as a favicon
50
- - Using preprocessor `lighten()` / `darken()` to derive shades
78
+ - Designing layouts/navs/shells before designing real features [source: references/01-foundations.md]
79
+ - Using font size as the only tool for hierarchy (ignoring weight and color) [source: references/02-page-mechanics.md]
80
+ - Using opacity to create grey text on colored backgrounds [source: references/02-page-mechanics.md]
81
+ - Starting with too little white space and adding it later [source: references/02-page-mechanics.md]
82
+ - Using `em` for type scales (compounds when nested) [source: references/02-page-mechanics.md]
83
+ - Using color as the only signal for a UI state [source: references/03-visual-treatment.md]
84
+ - Designing with placeholder images instead of real content [source: references/03-visual-treatment.md]
85
+ - Shrinking a logo down to use as a favicon [source: references/03-visual-treatment.md]
86
+ - Using preprocessor `lighten()` / `darken()` to derive shades [source: references/03-visual-treatment.md]
51
87
 
52
88
  ## Attribution
53
89
 
@@ -0,0 +1,124 @@
1
+ # Motion: Microinteractions, Transitions, Hover
2
+
3
+ > **Scope note**: motion is **not** covered in *Refactoring UI* by Adam Wathan & Steve Schoger. This file is complementary guidance, written in the same spirit as the book — favor restraint, build from a small fixed system, and avoid effects that exist to draw attention to themselves. It does not paraphrase the book.
4
+
5
+ ---
6
+
7
+ ## The Restraint Principle Carries Over
8
+
9
+ The book's central idea — "make the right things stand out by quieting the rest" — applies directly to motion. Animation in a UI is for **communicating state changes**, not for entertainment.
10
+
11
+ - If the user wouldn't notice the animation is gone, it shouldn't be there
12
+ - If the animation shows that something happened (state change, success, error), keep it
13
+ - "Wow" animations are a tax on every interaction after the first
14
+
15
+ ## Pre-Build a Motion System
16
+
17
+ Like type/color/spacing scales, define a fixed menu of durations and easings up front. Pick from that menu instead of inventing per-component.
18
+
19
+ **Sample duration scale (in milliseconds):**
20
+
21
+ | Token | Duration | Use |
22
+ |---|---|---|
23
+ | `instant` | 0–80 | Press/active states. Should feel like no delay. |
24
+ | `fast` | 100–200 | Hover effects, small state changes (icons, toggles, focus rings) |
25
+ | `normal` | 200–400 | Modal/drawer entry, expand/collapse, page-level transitions |
26
+ | `slow` | 400–600 | Decorative reveals, onboarding sequences |
27
+ | `+1s` | ≥ 1000 | Almost always wrong in a working UI — reserve for very specific moments |
28
+
29
+ **Sample easing scale:**
30
+
31
+ | Token | Curve | Use |
32
+ |---|---|---|
33
+ | `enter` | `cubic-bezier(0, 0, 0.2, 1)` (ease-out) | Elements appearing — fast start, slow finish |
34
+ | `exit` | `cubic-bezier(0.4, 0, 1, 1)` (ease-in) | Elements leaving — slow start, fast finish (they "fall away") |
35
+ | `standard` | `cubic-bezier(0.4, 0, 0.2, 1)` (ease-in-out) | State changes that aren't directional |
36
+ | `linear` | linear | Loaders, progress, anything tied to real time |
37
+
38
+ ## Animate Cheap Properties
39
+
40
+ Only `transform` and `opacity` are GPU-composited and frame-stable. Animating other properties forces layout/paint per frame and stutters on mid-range hardware.
41
+
42
+ | Cheap | Expensive |
43
+ |---|---|
44
+ | `transform: translate(...)`, `scale(...)`, `rotate(...)` | `width`, `height`, `top`, `left`, `margin`, `padding` |
45
+ | `opacity` | `box-shadow` (animatable, but slow at large blur) |
46
+ | `filter: blur()` (sparingly) | `background-color` on large surfaces |
47
+
48
+ To slide a panel in: animate `transform: translateX(100%)` → `translateX(0)`, not `right: -400px` → `right: 0`.
49
+
50
+ ## Hover States
51
+
52
+ The book's principle (use color/weight/contrast for hierarchy, not just one tool) applies to hover too — combine signals subtly.
53
+
54
+ A solid hover combines two of:
55
+
56
+ - A small lightness shift on the background (1–2 steps on your shade scale)
57
+ - A border or shadow change
58
+ - An icon color change
59
+
60
+ Avoid:
61
+
62
+ - Scaling up the whole element (1.05×, 1.1×) — chunky and dated
63
+ - A bright glow — feels like 2008
64
+ - Animating font size on hover — text reflow is jarring
65
+
66
+ ```css
67
+ .button {
68
+ background: var(--brand-500);
69
+ transition: background-color 150ms cubic-bezier(0, 0, 0.2, 1);
70
+ }
71
+ .button:hover {
72
+ background: var(--brand-600);
73
+ }
74
+ ```
75
+
76
+ ## Press / Active States
77
+
78
+ Visual pressure should feel like the element was pushed *into* the page — the inverse of "raised."
79
+
80
+ - Shadow shrinks or disappears
81
+ - A tiny `transform: translateY(1px)` works for cards
82
+ - The hover background usually deepens one more step on press
83
+
84
+ Press states should feel **instant** — duration `instant` (0–80ms), not `fast`.
85
+
86
+ ## Loading
87
+
88
+ In order of preference:
89
+
90
+ 1. **Optimistic UI** — assume success, update immediately, roll back on error. No spinner.
91
+ 2. **Skeleton screen** — placeholder shapes matching the final layout. Communicates structure while content loads.
92
+ 3. **Inline progress** — a thin progress bar at the top of the affected region, or shimmer on a single field.
93
+ 4. **Spinner** — only when 1–3 don't fit. Add a "Cancel" if the operation is long.
94
+
95
+ Skeleton screens beat spinners because they reduce the perceived wait — the layout doesn't shift when content arrives.
96
+
97
+ If you must use a spinner, **only show it after ~150ms** of waiting. Most operations finish faster than that; a spinner that flashes in and out is worse than no spinner.
98
+
99
+ ## Respect `prefers-reduced-motion`
100
+
101
+ Some users disable motion at the OS level (motion sickness, vestibular disorders, focus). The browser exposes this preference:
102
+
103
+ ```css
104
+ @media (prefers-reduced-motion: reduce) {
105
+ * {
106
+ animation-duration: 0.01ms !important;
107
+ animation-iteration-count: 1 !important;
108
+ transition-duration: 0.01ms !important;
109
+ scroll-behavior: auto !important;
110
+ }
111
+ }
112
+ ```
113
+
114
+ Or, more deliberately, swap motion for an instant state change inside specific components. Never disable a state change entirely (the user still needs to see that something happened) — only the *animation between* states.
115
+
116
+ ## Anti-Patterns
117
+
118
+ - ❌ Animating layout-affecting properties (`width`, `top`, `margin`) — jank
119
+ - ❌ One-off durations per component — kills consistency
120
+ - ❌ Easing on a linear curve for entries — feels mechanical
121
+ - ❌ Spinners that appear under 150ms — flashy noise
122
+ - ❌ Decorative animations that fire on every page load — annoying by the second visit
123
+ - ❌ Ignoring `prefers-reduced-motion` — accessibility regression
124
+ - ❌ Long durations (>600ms) for state changes — feels sluggish
@@ -0,0 +1,117 @@
1
+ # Dark Mode
2
+
3
+ > **Scope note**: *Refactoring UI* was written before dark mode was an industry default and does not address it explicitly. **However**, the book's HSL chapter (in `03-visual-treatment.md`) — handpicking shades by hue/saturation/lightness, not deriving them via `lighten()`/`darken()` — is exactly the foundation dark mode needs. This file extends those principles to a second color mode.
4
+
5
+ ---
6
+
7
+ ## Don't Invert — Rebuild
8
+
9
+ Inverting a light theme produces a "negative" image: muddy colors, weird text contrast, accents that lose punch. Dark mode is its own design pass.
10
+
11
+ The bookkeeping change: every color token now has a **mode pair**. Plan for it from the start, even if you only ship light first.
12
+
13
+ ```css
14
+ :root {
15
+ --bg-canvas: hsl(220 14% 96%);
16
+ --bg-surface: hsl(0 0% 100%);
17
+ --text-primary: hsl(220 13% 18%);
18
+ --text-secondary: hsl(220 9% 46%);
19
+ --border: hsl(220 13% 91%);
20
+ --brand-500: hsl(217 91% 60%);
21
+ }
22
+
23
+ :root[data-theme='dark'] {
24
+ --bg-canvas: hsl(220 13% 10%);
25
+ --bg-surface: hsl(220 13% 14%);
26
+ --text-primary: hsl(220 14% 96%);
27
+ --text-secondary: hsl(220 9% 65%);
28
+ --border: hsl(220 13% 22%);
29
+ --brand-500: hsl(217 91% 65%); /* nudged lighter for dark bg */
30
+ }
31
+ ```
32
+
33
+ ## Dark Backgrounds Aren't Black
34
+
35
+ Pure `#000` reads as a hole in the screen. Pull the lightness up to ~8–12%, keep a small amount of saturation, and lean slightly warm or cool depending on your brand.
36
+
37
+ | Backdrop | Lightness | Notes |
38
+ |---|---|---|
39
+ | App canvas (outermost) | 8–12% | Lowest layer — slightly tinted, not pure black |
40
+ | Surface (cards, panels) | 12–16% | One step above canvas — implies "raised" without a shadow |
41
+ | Elevated surface (modals, popovers) | 16–22% | Highest layer — even lighter |
42
+
43
+ The trick: in dark mode, **lighter = closer to the viewer**, the opposite of light mode. A higher elevation doesn't get a darker shadow — it gets a lighter surface.
44
+
45
+ ## Text Contrast in Dark Mode
46
+
47
+ Pure white text on a near-black background is *too much* contrast — the text vibrates and reading becomes tiring at length.
48
+
49
+ - Primary text: ~96% lightness (slightly off-white) — e.g., `hsl(220 14% 96%)`
50
+ - Secondary text: ~65% lightness — significantly muted compared to light mode's secondary
51
+ - Tertiary / placeholder: ~45–50%
52
+
53
+ WCAG ratios still apply (4.5:1 small text, 3:1 large), and slightly off-white still passes against a near-black canvas with room to spare.
54
+
55
+ ## Accent Colors Usually Need a Lightness Bump
56
+
57
+ A brand color tuned for white backgrounds often looks too dark and dim against a dark canvas. Most palettes need the accent **lightness raised 5–10%** for the dark variant.
58
+
59
+ Test against both backgrounds. If the brand color fails contrast in dark mode, derive a lighter variant rather than swapping for an unrelated color — keeps brand identity coherent.
60
+
61
+ ## Shadows: Mostly Gone, Sometimes Inverted
62
+
63
+ Shadows are made of light obstruction. On a dark canvas, the absence of light is the canvas itself — a dark shadow on dark background is invisible.
64
+
65
+ Options:
66
+
67
+ 1. **Skip shadows entirely**: use lighter surfaces and tinted borders to communicate elevation
68
+ 2. **Top highlight**: an inset top border (lighter color) to show that the element catches the imagined light source, with no shadow below
69
+ 3. **Very soft dark shadow**: works only when the element is on a lighter surface (e.g., a dropdown over the canvas) — large blur, low opacity
70
+
71
+ A `box-shadow: 0 2px 4px rgba(0,0,0,0.3)` that was crisp in light mode is invisible on a `hsl(220 13% 10%)` canvas. Recheck every shadow when adding dark mode.
72
+
73
+ ## Images and Photos in Dark Mode
74
+
75
+ Photos with bright backgrounds (typically light themes) "blow out" against a dark canvas — they pop too hard. Two tactics:
76
+
77
+ - **Lower image brightness slightly** in dark mode (e.g., `filter: brightness(0.9)`) — only for decorative/hero images, never for content the user is reading
78
+ - **Frame with a subtle border** — separates the bright rectangle from the dark canvas without dimming the photo
79
+
80
+ Same principle for video posters and avatar images.
81
+
82
+ ## SVG Icons and Logos
83
+
84
+ Single-color icons should be currentColor-driven so they pick up the mode's text color:
85
+
86
+ ```html
87
+ <svg fill="currentColor" ...>
88
+ ```
89
+
90
+ Multi-color logos need a dark variant. Don't fake it with `filter: invert()` — colors land in the wrong hue and brand recognition suffers.
91
+
92
+ ## Theme Toggle: Three Defaults
93
+
94
+ 1. **Match system preference** (`prefers-color-scheme`) — best first-paint experience
95
+ 2. **Respect explicit user override** — store in `localStorage`; persists across sessions
96
+ 3. **Apply before first paint** — set the theme class on `<html>` from a tiny inline script in `<head>` BEFORE any rendering, to avoid a flash of the wrong theme
97
+
98
+ ```html
99
+ <script>
100
+ (function () {
101
+ var stored = localStorage.getItem('theme');
102
+ var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
103
+ var theme = stored || (prefersDark ? 'dark' : 'light');
104
+ document.documentElement.dataset.theme = theme;
105
+ })();
106
+ </script>
107
+ ```
108
+
109
+ ## Anti-Patterns
110
+
111
+ - ❌ Pure black (`#000`) for the canvas — feels like a void, hurts at length
112
+ - ❌ Pure white text — vibrates against dark bg, tiring to read
113
+ - ❌ `filter: invert(1)` to "do" dark mode — produces wrong hues, breaks images
114
+ - ❌ Same brand color in both modes without checking contrast — fails WCAG silently
115
+ - ❌ Keeping every shadow from light mode unchanged — invisible against dark bg
116
+ - ❌ Theme toggle applied AFTER first paint — flash of opposite theme on every load
117
+ - ❌ Inverting photos and screenshots — colors shift unnaturally, recognition suffers