@maccesar/aiskills 1.12.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.
- package/README.md +42 -7
- package/lib/cleanup.js +29 -0
- package/lib/commands/skills.js +110 -9
- package/lib/config.js +17 -9
- package/lib/installer.js +5 -3
- package/lib/symlink.js +45 -3
- package/lib/utils.js +41 -0
- package/package.json +1 -1
- package/skills/audit-codebase/SKILL.md +70 -0
- package/skills/audit-codebase/agents/openai.yaml +4 -0
- package/skills/audit-codebase/references/comprehensive-audit.md +220 -0
- package/skills/audit-codebase/references/report-format.md +119 -0
- package/skills/humaniza/SKILL.md +55 -4
- package/skills/humaniza/references/ai-patterns-es.md +40 -0
- package/skills/humaniza/references/checklist.md +9 -0
- package/skills/humaniza/references/examples.md +16 -0
- package/skills/humaniza/references/lexicon-es-mx.md +18 -0
- package/skills/humaniza/references/structures-es.md +132 -0
- package/skills/humaniza/scripts/check_ai_patterns.py +216 -0
- package/skills/refactoring-ui/SKILL.md +65 -29
- package/skills/refactoring-ui/references/05-motion.md +124 -0
- package/skills/refactoring-ui/references/06-dark-mode.md +117 -0
- package/skills/refactoring-ui/references/07-component-patterns.md +181 -0
- package/skills/stitch-showcase/SKILL.md +24 -232
- package/skills/stitch-showcase/references/07-theme-system.md +12 -0
- package/skills/stitch-showcase/references/08-type-detection.md +9 -1
- package/skills/stitch-showcase/references/10-component-standardization.md +25 -0
- package/skills/stitch-showcase/references/12-video-embedding.md +113 -0
- package/skills/stitch-showcase/references/13-language-detection.md +82 -0
- package/skills/stitch-showcase/references/14-troubleshooting-known-issues.md +122 -0
- package/skills/stitch-showcase/references/15-build-flags.md +71 -0
- package/skills/stitch-showcase/references/16-design-md-format.md +107 -0
- package/skills/stitch-showcase/references/index.html +25 -19
- package/skills/stitch-showcase/references/viewer.html +24 -12
- package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/slug_demangle.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/build_showcase.py +150 -10
- package/skills/stitch-showcase/scripts/parse_design_md.py +145 -12
- package/skills/stitch-showcase/scripts/slug_demangle.py +209 -0
- package/skills/vscode-extension-dev/SKILL.md +90 -41
- package/skills/vscode-extension-dev/references/api-additional.md +168 -0
- package/skills/vscode-extension-dev/references/api-progress.md +55 -0
- package/skills/vscode-extension-dev/references/api-quickpick.md +75 -0
- package/skills/vscode-extension-dev/references/api-secretstorage.md +57 -0
- package/skills/vscode-extension-dev/references/api-statusbar.md +38 -0
- package/skills/vscode-extension-dev/references/api-treeview.md +78 -0
- package/skills/vscode-extension-dev/references/api-webview.md +149 -0
- package/skills/vscode-extension-dev/references/architecture.md +67 -0
- package/skills/vscode-extension-dev/references/debugger.md +179 -0
- package/skills/vscode-extension-dev/references/lsp.md +175 -0
- package/skills/vscode-extension-dev/references/notebooks.md +208 -0
- package/skills/vscode-extension-dev/references/testing.md +208 -0
- 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:
|
|
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
|
-
##
|
|
10
|
+
## Required workflow (read before responding)
|
|
25
11
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|