command-code 1.24.0 → 1.26.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/CHANGELOG.md +16 -0
- package/dist/bundled/command-code-knowledge/reference/models.md +1 -0
- package/dist/bundled/command-code-knowledge/reference/product-help.md +1 -0
- package/dist/bundled/design/SKILL.md +24 -4
- package/dist/bundled/design/references/accessibility.md +404 -0
- package/dist/bundled/design/references/border.md +6 -0
- package/dist/bundled/design/references/checkup.md +2 -0
- package/dist/bundled/design/references/color.md +69 -0
- package/dist/bundled/design/references/interaction.md +7 -0
- package/dist/bundled/design/references/layout.md +57 -0
- package/dist/bundled/design/references/motion.md +35 -0
- package/dist/bundled/design/references/review.md +3 -1
- package/dist/bundled/design/references/severity.md +111 -0
- package/dist/bundled/design/references/shadow.md +27 -0
- package/dist/bundled/design/references/smell.md +8 -0
- package/dist/bundled/design/references/typeset.md +30 -0
- package/dist/bundled/design/references/writing.md +22 -0
- package/dist/cli.mjs +4 -4
- package/package.json +4 -4
- package/vsix/commandcode-vscode.vsix +0 -0
|
@@ -12,6 +12,7 @@ Interaction drives the behavior pass — consult these when each dimension comes
|
|
|
12
12
|
|
|
13
13
|
- [button.md](button.md) — for correct button states and the full control system when auditing interactive elements
|
|
14
14
|
- [motion.md](motion.md) — for correct transition timing and easing when specifying state-change animation
|
|
15
|
+
- [accessibility.md](accessibility.md) — for the mechanics behind every behavior in this file: focus rings, tabindex and roving tabindex, focus trapping and restoration, APG keyboard patterns, form labels and error announcement, live regions, and hit-area math
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
@@ -85,6 +86,10 @@ On touch devices, controls need enough room for fingers, not cursors. Adjacent l
|
|
|
85
86
|
|
|
86
87
|
If a feature only exists on hover, it does not exist for touch users.
|
|
87
88
|
|
|
89
|
+
The floor is 24×24px, the working target is 44×44px for touch and 40×40px for desktop when density permits. A small visible control keeps its size and gets an expanded hit area from a pseudo-element on the wrapping label or button — never on the `<input>`, which does not render pseudo-elements reliably. Two interactive elements never have overlapping hit areas: if an expanded area would collide, I shrink it to the largest size that does not. Exact math, exceptions, and recipes: [accessibility.md](accessibility.md#hit-areas).
|
|
90
|
+
|
|
91
|
+
I detect input mode rather than screen size — `pointer: coarse` for sizing, `hover: hover` for hover affordances — and add `touch-action: manipulation` so taps do not wait out the double-tap-zoom delay.
|
|
92
|
+
|
|
88
93
|
---
|
|
89
94
|
|
|
90
95
|
## Direction-Aware Controls
|
|
@@ -113,6 +118,8 @@ Labels stay visible. Placeholders show examples, not identity. Required fields a
|
|
|
113
118
|
|
|
114
119
|
Every error answers what happened and how to recover. Blame is forbidden.
|
|
115
120
|
|
|
121
|
+
I keep submit enabled until the request starts. A submit button disabled until the form is valid hides the very thing that must be fixed — I let the user submit, then focus the first invalid field, which is both the fix path and the announcement. Once the request starts, the button disables with a spinner and **keeps its original label**. Every field gets the right `type`, an `inputmode` that summons the right mobile keyboard, and an `autocomplete` token so password managers and one-time-code autofill work. I never block paste, and I never filter characters as the user types — I accept the input, trim it, and validate after. Markup and announcement mechanics: [accessibility.md](accessibility.md#forms).
|
|
122
|
+
|
|
116
123
|
On mobile, I verify every `input`, `select`, and `textarea` uses a font-size of at least 16px. A sub-16px form element triggers iOS Safari auto-zoom on focus, shifting layout and forcing the user to pinch back — it breaks the interaction, not just the visuals. Fix guidance and the specificity gotcha: [responsive.md](responsive.md#ios-safari-input-zoom).
|
|
117
124
|
|
|
118
125
|
---
|
|
@@ -119,10 +119,67 @@ I avoid dead-perfect section endings on long pages. A hint of the next section k
|
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
122
|
+
## Layout Mechanics
|
|
123
|
+
|
|
124
|
+
**Group with space, not lines.** Three tools, in order of preference: negative space, then a background shape when a group must read as one unit, then a separator line — last, and only where space would cost too much (dense tables, long settings lists). The structural rule: **the gap between groups is at least 2× the gap within a group.** 8px inside, 16px+ between. Below that ratio the grouping reads as noise, and adding a line to compensate is treating the symptom.
|
|
125
|
+
|
|
126
|
+
A line that has genuinely earned its place still keeps its voice down — thin, dim, and never sitting inside a wide gap. If the space is already doing the separating, the line is just repeating it louder.
|
|
127
|
+
|
|
128
|
+
**Controls must not look like content.** An interactive element needs a background, a border, an underline, or a consistent control zone. A link styled exactly like the sentence around it is invisible. The inverse holds too: a static badge shaped like the buttons beside it collects dead clicks.
|
|
129
|
+
|
|
130
|
+
**Shared edges.** I pick a small set of alignment edges and put everything on them. Every stray edge — an icon 2px off the text edge, a card padded differently from its neighbor — reads as noise even when nobody can name it. One spacing step expresses each level of subordination, and deeper nesting repeats the same step rather than inventing a new one.
|
|
131
|
+
|
|
132
|
+
**Logical properties, not physical.** Direction-dependent position is written as leading/trailing so the layout mirrors under `dir="rtl"`:
|
|
133
|
+
|
|
134
|
+
| Physical (avoid) | Logical (use) |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `margin-left` | `margin-inline-start` |
|
|
137
|
+
| `padding-right` | `padding-inline-end` |
|
|
138
|
+
| `left: 0` | `inset-inline-start: 0` |
|
|
139
|
+
| `text-align: left` | `text-align: start` |
|
|
140
|
+
| `border-right` | `border-inline-end` |
|
|
141
|
+
|
|
142
|
+
Physical properties are reserved for genuinely physical geometry: a device notch, a gesture direction. When an arrangement encodes progression — star ratings, step indicators, progress bars — the sequence mirrors too; flexbox and grid with logical properties do it automatically, hand-positioned elements do not.
|
|
143
|
+
|
|
144
|
+
**Breathing room between targets.** Without an established density scale: `12px` between adjacent bordered or filled controls, `24px` around borderless text and icon buttons, `24px`+ between unrelated groups. Borderless controls need more because nothing marks where one target ends and the next begins — the space *is* the boundary. Compact professional tools may use less, as long as hit areas stay distinct and never overlap ([accessibility.md](accessibility.md#hit-areas)). I preserve an established, usable density rather than inflating controls to match a number.
|
|
145
|
+
|
|
146
|
+
**Progressive disclosure needs an affordance.** Hiding complexity is good; hiding it with no cue is a trap. In a horizontal scroller, items are sized so the next one peeks `16–32px` past the edge — a row that ends exactly at the container edge looks complete, and nobody scrolls it. Collapsed sections get a control whose label states what is hidden ("Show 12 more results", not "More"). Clamped text shows an ellipsis *and* a way to expand.
|
|
147
|
+
|
|
148
|
+
**Content bleeds, controls float.** Backgrounds, hero media, and scrollable lists extend to the viewport edges. Text and controls stay inside the layout margins and safe areas. Sticky chrome floats above the content layer; it does not dam it.
|
|
149
|
+
|
|
150
|
+
```css
|
|
151
|
+
/* Full-bleed media inside a constrained article */
|
|
152
|
+
.article { display: grid; grid-template-columns: 1fr min(65ch, calc(100% - 48px)) 1fr; }
|
|
153
|
+
.article > * { grid-column: 2; }
|
|
154
|
+
.article > .full-bleed { grid-column: 1 / -1; }
|
|
155
|
+
|
|
156
|
+
/* Floating chrome respects the notch */
|
|
157
|
+
.fab {
|
|
158
|
+
position: fixed;
|
|
159
|
+
inset-inline-end: calc(16px + env(safe-area-inset-right));
|
|
160
|
+
bottom: calc(16px + env(safe-area-inset-bottom));
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Breakpoints come from content.** I break where the layout actually stops fitting — where the sidebar squeezes content below its minimum measure, where the card grid drops below a usable column width — not at 768px because a preset says so. I collapse late: a layout that holds its expanded structure as long as it genuinely fits stays stable and familiar, and premature collapsing throws away space the user paid for. I test the smallest and largest supported sizes first, because those break first.
|
|
165
|
+
|
|
166
|
+
**Plan for growth and clipping.** String expansion varies by language and by source-string length, so no universal percentage saves me. No fixed widths sized to English labels — `max-width` plus wrapping. No fixed heights on text containers — `min-height` if a floor is needed. Buttons size from their label via `padding-inline`, never a hardcoded width. And I never park a critical action where resizing, scrolling, or an expanding keyboard can clip it: primary actions live in the normal flow or in stable chrome with safe-area padding. If a modal's content scrolls, its action row does not.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
122
170
|
## Container Sense
|
|
123
171
|
|
|
124
172
|
Components should know the space they live in. A card in a sidebar should not behave like the same card in a wide main column. Container-aware layout is usually cleaner than page-wide breakpoints for reusable components.
|
|
125
173
|
|
|
174
|
+
```css
|
|
175
|
+
/* The component adapts to its column */
|
|
176
|
+
.card-list { container-type: inline-size; }
|
|
177
|
+
@container (max-width: 400px) { .card { grid-template-columns: 1fr; } }
|
|
178
|
+
|
|
179
|
+
/* A viewport query breaks the same card inside a narrow sidebar */
|
|
180
|
+
@media (max-width: 768px) { .card { grid-template-columns: 1fr; } }
|
|
181
|
+
```
|
|
182
|
+
|
|
126
183
|
Viewport rules still matter for page shell decisions. Component composition belongs closer to the component.
|
|
127
184
|
|
|
128
185
|
---
|
|
@@ -131,6 +131,41 @@ If the reduced version loses meaning, the original motion was carrying too much
|
|
|
131
131
|
- Full motion: Element slides in over 400ms
|
|
132
132
|
- Reduced motion: Element fades in over 150ms (no translation)
|
|
133
133
|
|
|
134
|
+
I write motion as **opt-in**, so the static version is the default and I am not chasing every animation with an override:
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
.card { /* static styles */ }
|
|
138
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
139
|
+
.card { transition: transform 200ms ease-out; }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In an existing codebase where opt-in is not feasible, the global kill switch uses `0.01ms` rather than `none`, so `transitionend` and `animationend` still fire and any JS waiting on them does not hang.
|
|
144
|
+
|
|
145
|
+
The preference asks for less motion, not none. What it protects against is vestibular upset, not the interface answering the user:
|
|
146
|
+
|
|
147
|
+
| Cut it | Swap it | Leave it alone |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| Parallax | Anything that slides, scales, or zooms → a plain opacity crossfade | Spinners and progress indicators |
|
|
150
|
+
| Video, GIFs, and looping decoration that start themselves | Smooth scrolling → jump straight there | State that changes instantly: hover color, the focus ring |
|
|
151
|
+
| Spinning, and anything travelling a long way across the screen | Carousels that rotate on their own → hand them over paused | The short confirmation that a press registered |
|
|
152
|
+
|
|
153
|
+
Independent of the preference: anything moving, blinking, or updating on its own for more than 5 seconds needs a visible pause control. Every animation stays interruptible and driven by user input.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Enter And Exit Mechanics
|
|
158
|
+
|
|
159
|
+
For an infrequent staged entrance — a page hero, a success state, an empty state — I split the container into semantic chunks (title, description, action) and stagger them by ~100ms, combining `opacity`, a small `translateY`, and `blur`. Titles can go a level finer, word by word at ~80ms. I do not stagger routine, high-frequency interactions; the attention cost repeats on every trigger.
|
|
160
|
+
|
|
161
|
+
Exits are softer than enters and run shorter — roughly 150ms against a 300ms entrance. A small fixed `translateY` (about `-12px`) indicates direction without drama; the full container height is theatre. When motion adds no information, or the interaction repeats often, removing the element immediately is the correct exit.
|
|
162
|
+
|
|
163
|
+
I use CSS transitions for interactive state changes, because they can be interrupted mid-flight, and reserve keyframes for staged sequences that run once. I transition exact properties (`transition-property: opacity, scale`), never `all`. `will-change` is for `transform`, `opacity`, and `filter` only, added when I actually see first-frame stutter — never `will-change: all`.
|
|
164
|
+
|
|
165
|
+
**Icon swaps** animate with `opacity`, `scale`, and `blur` rather than toggling visibility: scale `0.25` → `1`, opacity `0` → `1`, blur `4px` → `0`. With a motion library, `{ type: "spring", duration: 0.3, bounce: 0 }` — bounce stays `0`. Without one, both icons stay in the DOM (one absolutely positioned) and cross-fade with `cubic-bezier(0.2, 0, 0, 1)`, which gives enter and exit with no dependency.
|
|
166
|
+
|
|
167
|
+
Nothing is communicated by movement alone. Anything I animate to mark a change also has to read as changed once it stops moving — through color, a glyph, or words.
|
|
168
|
+
|
|
134
169
|
---
|
|
135
170
|
|
|
136
171
|
## Timing Reference
|
|
@@ -111,10 +111,12 @@ I do not inflate scores to be polite. Most real work lives in the middle.
|
|
|
111
111
|
|
|
112
112
|
I do not leave the user with a pile of observations.
|
|
113
113
|
|
|
114
|
-
I name the top improvements in order of impact and map each to the right Command Code design mode: recolor, typeset, relayout, interaction, writing, refine, finish, redesign, or create.
|
|
114
|
+
I name the top improvements in order of impact and map each to the right Command Code design mode: recolor, typeset, relayout, interaction, a11y, writing, refine, finish, redesign, or create.
|
|
115
115
|
|
|
116
116
|
If the fix is not a design-mode issue, I say that too.
|
|
117
117
|
|
|
118
|
+
Recommendations use the severity scale, evidence bar, findings table, considered-but-rejected list, and verdict in [severity.md](severity.md). An escalation trigger from that file outranks every aesthetic finding in the report, regardless of what the lens scores say — a surface that scores well on all five lenses and cannot be operated with a keyboard is a `Block`.
|
|
119
|
+
|
|
118
120
|
---
|
|
119
121
|
|
|
120
122
|
## Report Boundary
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Findings: severity, evidence, verdict
|
|
2
|
+
|
|
3
|
+
Every report mode — `smell`, `checkup`, `review` — uses this scale, this evidence bar, and this verdict. Scores answer "how good is this?". Findings answer "what do I fix, in what order?". A report needs both.
|
|
4
|
+
|
|
5
|
+
A finding the user cannot act on is an observation. This file turns observations into work.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Severity
|
|
10
|
+
|
|
11
|
+
One scale, three levels.
|
|
12
|
+
|
|
13
|
+
**HIGH** — blocks a task, misleads the user, hides content or controls, risks data loss, or repeats as a systemic failure across the surface.
|
|
14
|
+
|
|
15
|
+
**MEDIUM** — meaningfully harms comprehension, efficiency, adaptability, or consistency. The user gets there, but pays for it.
|
|
16
|
+
|
|
17
|
+
**LOW** — isolated polish with limited task impact.
|
|
18
|
+
|
|
19
|
+
Within a severity I rank by reach and leverage. A fix in a token or a shared component outranks the same symptom in one leaf component, because it fixes every instance at once.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Escalation Triggers
|
|
24
|
+
|
|
25
|
+
These are `HIGH` on sight. I do not average them down because the surface is minor, and I do not hold them back to keep a report short.
|
|
26
|
+
|
|
27
|
+
- Something a user can operate that never says what it is
|
|
28
|
+
- Something Tab can land on with nothing visible to show it landed
|
|
29
|
+
- A path the mouse can walk and the keyboard cannot
|
|
30
|
+
- Decorative or vestibular motion — parallax, self-starting video/GIFs, spinning, long-travel movement, autoplaying carousels — that runs regardless of `prefers-reduced-motion`
|
|
31
|
+
- Anything cut off, buried, or out of reach once the window narrows to 320px or the text doubles
|
|
32
|
+
- Text sitting on a background it does not have enough contrast against
|
|
33
|
+
- A distinction the user can only make by seeing a hue
|
|
34
|
+
- Something irreversible with no confirmation, no undo, and nothing marking it apart from a safe action
|
|
35
|
+
- A field whose placeholder is doing the label's job
|
|
36
|
+
|
|
37
|
+
Triggers rank above every other finding and are listed first. Mechanics and fixes: [accessibility.md](accessibility.md).
|
|
38
|
+
|
|
39
|
+
These set the *cost* of a symptom, not whether it exists. The discipline reference still decides whether the symptom is present.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Evidence
|
|
44
|
+
|
|
45
|
+
Every finding cites `path/to/file:line` and shows the current implementation. When the artifact has no source files, I cite the exact screen and component instead.
|
|
46
|
+
|
|
47
|
+
I do not report a code-level finding from appearance alone, or a visual finding from source alone when runtime behavior decides the result. If I could not check something, it is not a finding — it is a verification gap, and I say so.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Findings Table
|
|
52
|
+
|
|
53
|
+
Findings go in one table, ordered by severity, then by reach and leverage. Never as loose "Before:" / "After:" lines.
|
|
54
|
+
|
|
55
|
+
| # | Severity | Discipline | Location | Before | After | Why |
|
|
56
|
+
|---|---|---|---|---|---|---|
|
|
57
|
+
| 1 | HIGH | Accessibility | `src/CommandBar.tsx:88` | `<div onClick={run}><PlayIcon /></div>` | Make it a `<button>`, name it `Run command`, hide the glyph from the tree | Nothing here is focusable, and nothing announces what it does |
|
|
58
|
+
| 2 | HIGH | Color | `src/Badge.tsx:14` | Status shown as `bg-red-500` / `bg-green-500` only | Add an icon and a text label beside the fill | Meaning carried by color alone |
|
|
59
|
+
| 3 | MEDIUM | Layout | `src/Toolbar.tsx:22` | Five icon buttons at `gap-1` | Raise to the project's `12px` step | Adjacent targets merge and get mis-tapped |
|
|
60
|
+
|
|
61
|
+
**Discipline** is the reference that owns the rule: Accessibility, Color, Type, Layout, Motion, Interaction, Writing, Surface, Voice. One rule, one owner — when two references seem to cover an issue, it belongs to whichever owns the underlying rule, and the secondary effect goes in the **Why** cell. I report it once.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Consolidate
|
|
66
|
+
|
|
67
|
+
One root cause is one finding. Twelve components missing focus rings because a shared button strips the outline is one row with twelve locations, not twelve rows.
|
|
68
|
+
|
|
69
|
+
I never pad a report to look thorough. A short findings list is a valid result, and so is an empty one.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Considered but Rejected
|
|
74
|
+
|
|
75
|
+
Every report includes 2–5 candidates I inspected and deliberately did not report. This is how the user can tell restraint from oversight.
|
|
76
|
+
|
|
77
|
+
| Location | Candidate | Rejected because |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `src/Card.tsx:28` | Increase the shadow | Depth already matches the shared surface token; changing one card would cost consistency |
|
|
80
|
+
|
|
81
|
+
I drop a candidate when the discipline reference allows what is already there, when I could not gather enough evidence to be sure, when the project's way of doing it holds up on its own merits rather than merely being the way it has always been done, or when my proposed change would cost complexity and buy the user nothing.
|
|
82
|
+
|
|
83
|
+
These are real candidates from this pass, not invented filler. If the surface genuinely had fewer borderline calls, I list the ones that exist and say so.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Verification
|
|
88
|
+
|
|
89
|
+
I list every check I ran, the exact command or interaction, and what I observed. Checks that passed and checks marked **Not verified** go in separate groups.
|
|
90
|
+
|
|
91
|
+
A verification gap never becomes a finding, and never becomes a silent pass either.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Verdict
|
|
96
|
+
|
|
97
|
+
Every report ends with exactly one:
|
|
98
|
+
|
|
99
|
+
- **Block** — at least one `HIGH` is still standing.
|
|
100
|
+
- **Needs changes** — nothing `HIGH` left, but `MEDIUM` or `LOW` work is outstanding.
|
|
101
|
+
- **Approve** — nothing left to act on, and I actually ran the checks I am claiming coverage from.
|
|
102
|
+
|
|
103
|
+
I do not write "Approve" with pending actionable findings, and I do not write "Block" for polish.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Report Modes Do Not Fix
|
|
108
|
+
|
|
109
|
+
`smell`, `checkup`, and `review` produce findings only. The fix happens when the user runs `redesign`, `relayout`, `recolor`, `typeset`, `a11y`, `deslop`, `refine`, or `finish` — a separate, explicit command.
|
|
110
|
+
|
|
111
|
+
The severity, the locations, and the **After** column are what those modes consume next. A finding written vaguely today becomes a fix nobody can apply tomorrow.
|
|
@@ -94,6 +94,33 @@ Flat systems use borders and maybe a whisper of shadow. Layered systems use shad
|
|
|
94
94
|
|
|
95
95
|
I choose the elevation strategy per project and stay with it.
|
|
96
96
|
|
|
97
|
+
When a border on a button, card, or container exists **only to create depth**, a layered transparent shadow does the job better: it adapts to any background, where a solid border color only works on the background it was picked for. Three layers — a 1px ring, a subtle lift, and ambient depth:
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
:root {
|
|
101
|
+
--shadow-border:
|
|
102
|
+
0 0 0 1px oklch(0 0 0 / 0.06),
|
|
103
|
+
0 1px 2px -1px oklch(0 0 0 / 0.06),
|
|
104
|
+
0 2px 4px 0 oklch(0 0 0 / 0.04);
|
|
105
|
+
--shadow-border-hover:
|
|
106
|
+
0 0 0 1px oklch(0 0 0 / 0.08),
|
|
107
|
+
0 1px 2px -1px oklch(0 0 0 / 0.08),
|
|
108
|
+
0 2px 4px 0 oklch(0 0 0 / 0.06);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
In dark mode this collapses to a single white ring — `0 0 0 1px oklch(1 0 0 / 0.08)`, hover `0.13` — because layered depth shadows are not visible on dark backgrounds.
|
|
113
|
+
|
|
114
|
+
This never applies to borders whose job is separation: dividers, table cell boundaries, hairlines in dense UI, and input outlines stay borders.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Image Outlines
|
|
119
|
+
|
|
120
|
+
Images get a `1px` low-opacity outline so they sit in the same depth system as everything else. `outline` rather than `border` because it never affects layout, and `outline-offset: -1px` draws the ring just inside the image edge so it hugs the corner radius instead of floating outside it.
|
|
121
|
+
|
|
122
|
+
The color is non-negotiable: **pure black in light mode** (`oklch(0 0 0 / 0.1)`), **pure white in dark mode** (`oklch(1 0 0 / 0.1)`). Never a near-black or tinted neutral from the palette — slate, zinc, `#111827`, `#f5f5f7` — and never the accent. A tinted outline picks up the surface color beneath it and reads as dirt on the image edge.
|
|
123
|
+
|
|
97
124
|
---
|
|
98
125
|
|
|
99
126
|
## Performance
|
|
@@ -153,6 +153,14 @@ The heuristics table in the report uses 10 rows (one per odor tracked). Each row
|
|
|
153
153
|
|
|
154
154
|
---
|
|
155
155
|
|
|
156
|
+
## Finding Format
|
|
157
|
+
|
|
158
|
+
Each confirmed tell gets a severity, a `path/to/file:line` location, and a concrete replacement, per [severity.md](severity.md). A repeated tell is one finding with every location listed, not one row per occurrence — the whole point of a smell is that it is systemic.
|
|
159
|
+
|
|
160
|
+
Smell also carries the escalation triggers in that file. Generic patterns and access failures travel together: a placeholder-only label, an icon button with no name, and a `focus:outline-none` on a shared component are all AI reflexes, and all of them are `HIGH`.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
156
164
|
## Report Boundary
|
|
157
165
|
|
|
158
166
|
Smell always produces two report artifacts:
|
|
@@ -137,6 +137,36 @@ I test it with real content, not a perfect headline.
|
|
|
137
137
|
|
|
138
138
|
---
|
|
139
139
|
|
|
140
|
+
## Type Mechanics
|
|
141
|
+
|
|
142
|
+
The taste above decides what to do. This decides how to write it. I write every fix in the project's own styling system, never a second one alongside it.
|
|
143
|
+
|
|
144
|
+
**Wrapping.** `text-wrap: balance` on headings, `text-wrap: pretty` on descriptions. Both, together, on a heading-plus-subtitle pair. Neither in long-form: browsers ignore `balance` past a few lines, and evening out a whole article wastes space and slows reading. `overflow-wrap: break-word` wherever a long word, URL, or ID could escape its container. `white-space: nowrap` on labels and badges where a break looks broken.
|
|
145
|
+
|
|
146
|
+
**Measure.** 60–75 characters. `65ch` measures it directly, but a rem or pixel cap is equally valid — at a 16px body size the range lands roughly between 560px and 680px, so Tailwind's `max-w-xl` or `max-w-2xl` fit. What matters is that a cap exists and the rendered line length sits in range; I recheck it if the body size changes.
|
|
147
|
+
|
|
148
|
+
**Line-height by role.** Headings near `1.1`, body `1.5`–`1.6`, always unitless so it scales with the font size — `line-height: 24px` does not. Tight leading is for short text: anything that wraps to three or more lines needs at least `1.4`, even in a height-constrained card.
|
|
149
|
+
|
|
150
|
+
**Weight floors.** Below `18px`, stay at weight `400` or heavier. Weights under `300` are display-only at `28px`+; they disappear at text sizes. Long-form body starts near `16px`; UI text can go to `14px` for inputs and menus, `13px` for captions, rarely below `12px`.
|
|
151
|
+
|
|
152
|
+
**Properties over raw tags.** `font-weight: 650`, not `font-variation-settings: "wght" 650`. `font-optical-sizing: auto`, not `"opsz"`. `font-variant-numeric: tabular-nums`, not `font-feature-settings: "tnum" 1`. Properties keep working when a non-variable fallback renders; raw settings silently do nothing. Raw tags are for custom axes (`"GRAD" 80`) and numbered slots (`"ss01"`) that have no property of their own.
|
|
153
|
+
|
|
154
|
+
**Synthesis.** Browsers may fake a weight or style the family does not ship. I load the faces the design actually uses. `font-synthesis: none` goes on only after I have verified that every required bold, italic, small-cap, superscript, and subscript form stays visually distinct across the whole fallback stack — it is not a diagnostic, and it must never erase emphasis. If only one mode is unwanted, I use the longhand.
|
|
155
|
+
|
|
156
|
+
**Truncation.** Single line is `text-overflow: ellipsis` with `overflow: hidden` and `white-space: nowrap`. Multiple lines is `line-clamp`. Truncation hides content, so if the missing text matters, the full value stays reachable in a tooltip or an expanded view.
|
|
157
|
+
|
|
158
|
+
**Underlines.** Pull position and thickness from the font with `text-underline-position: from-font` and `text-decoration-thickness: from-font`, or tune with `text-underline-offset` and `text-decoration-skip-ink`. A dotted underline is the conventional hint that a word carries extra information. Color is the only part of a real underline that animates reliably — anything else, I build the underline as a separate element.
|
|
159
|
+
|
|
160
|
+
**Punctuation.** Copy is stored in natural case and presented with `text-transform`, so a redesign never means rewriting strings. Curly quotes in prose and straight quotes in code. En dash for ranges (`2010–2020`). The single `…` character, not three periods. ` ` to keep `16 px` together, `­` to control where a long word may break. Em dashes stay out of generated UI copy for this project — see [writing.md](writing.md).
|
|
161
|
+
|
|
162
|
+
**Rendering and language.** `-webkit-font-smoothing: antialiased` plus `-moz-osx-font-smoothing: grayscale` once at the root, never per component. `lang` set so the browser picks the right quotes, hyphenation, and pronunciation; `dir` set at the boundary where direction changes. Digit order never reverses in RTL — I wrap mixed-direction values in `<bdi>` rather than fighting the bidi algorithm. A paragraph of three or more lines aligns to its own script's direction; `text-align: start` with correct `lang`/`dir` handles it.
|
|
163
|
+
|
|
164
|
+
**Selection stays on.** `user-select: none` belongs only on a specific draggable or gesture surface where accidental selection demonstrably breaks the interaction — never across application chrome because a button label can be highlighted.
|
|
165
|
+
|
|
166
|
+
**Mobile inputs.** iOS Safari zooms the page when an input's text is under `16px`. Two fixes hold the size differently, so I ask which the design wants rather than choosing silently: size the input up on small screens (`text-base sm:text-sm`), which changes how it looks, or keep `font-size: 16px` and render the intended size with `transform: scale()`, compensating width and line-height so the design is identical everywhere. Details: [responsive.md](responsive.md#ios-safari-input-zoom).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
140
170
|
## What I Refuse
|
|
141
171
|
|
|
142
172
|
- Choosing the first trendy family that fits the category
|
|
@@ -94,6 +94,26 @@ Icon-only controls need accessible names. Link text stands alone. Alt text descr
|
|
|
94
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
97
|
+
## Settings And Toggles
|
|
98
|
+
|
|
99
|
+
A toggle is labeled for what happens when it is **on**: "Send read receipts". The user infers the off state. Labeling the negative ("Don't send read receipts") turns every interaction into a double negative.
|
|
100
|
+
|
|
101
|
+
When copy refers to another setting, I link straight to it. A "Notification settings" link beats "Go to Settings > Notifications > Email", which goes stale the moment the navigation changes.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Links And Flow Vocabulary
|
|
106
|
+
|
|
107
|
+
Link text has to make sense out of context, because screen-reader users navigate by a list of the page's links. "Read the billing docs", never "Click here" — which also assumes a mouse. A bare "Learn more" fails the moment two of them appear on one page; each gets a suffix: "Learn more about exports".
|
|
108
|
+
|
|
109
|
+
A multi-step flow uses one vocabulary throughout: one word to enter, one word to advance — "Continue" or "Next", picked once and kept — and one word to finish. Alternating synonyms across steps makes users wonder whether the buttons do different things.
|
|
110
|
+
|
|
111
|
+
A confirmation's buttons restate what is about to happen, so the user can answer the dialog from the buttons alone without parsing the paragraph above them. "Delete this project?" is answered with **Delete project** and **Cancel** — never Yes and No, which force the user back up to the question to work out which one is which.
|
|
112
|
+
|
|
113
|
+
I address the reader as "you", not "the user". In errors I avoid "we", which reads as deflection: "Unable to load content" beats "We're having trouble loading this content".
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
97
117
|
## Voice And Tone
|
|
98
118
|
|
|
99
119
|
Voice stays consistent. Tone adapts to the moment.
|
|
@@ -110,6 +130,8 @@ I leave room for expansion. German and Finnish get longer. Chinese may use fewer
|
|
|
110
130
|
|
|
111
131
|
I keep full sentences as translation units. I avoid string fragments that assume English grammar. I avoid abbreviations when clarity matters.
|
|
112
132
|
|
|
133
|
+
I never build a sentence by concatenating fragments around a variable — `"You have " + n + " new messages"` assumes English word order and English plural rules, and breaks in most other languages. Full templated strings with proper pluralization instead. I also skip idioms, colloquialisms, and humor that will not survive translation, and I match the verb to the input device: "tap" on touch, "click" with a pointer, "select" when both are possible.
|
|
134
|
+
|
|
113
135
|
---
|
|
114
136
|
|
|
115
137
|
## Punctuation
|