Forbidden Patterns
The canonical list of OnRamp UI prohibitions — the single source of truth for what is never allowed in app/ui-organization/** (and migrated portal code). Each entry has a grep-able pattern, why it's wrong, and the fix.
Three surfaces reference this one list rather than restating it:
/design-handoff-buildself-checks generated output against it (.agents/skills/design-handoff-build/references/forbidden-patterns.md)./design-handoff-evalaudits against it (.agents/skills/design-handoff-eval/references/audit-procedure.md§3–4)..claude/rules/ui-design-system.mdstates the prohibitions tersely for always-on authoring and points here for the full grep-table form.
Token values (which ramp warn resolves to, the spacing scale) are not here — those live in DESIGN.md and app/ui-organization/theme/preset.js. This file lists prohibitions, not values.
1. Raw ramp utilities
Pattern:
\b(text|bg|border|ring|divide|fill|stroke|outline|accent|caret|placeholder|shadow|from|via|to)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone|black|white)-(?:50|100|200|300|400|500|600|700|800|900|950)\bWhy bad: Bypasses the preset. Doesn't track scheme changes. Won't theme correctly when the preset evolves. (The color ramp is no longer emitted into @theme, so most of these utilities don't even exist anymore.)
Fix: Use a semantic primeui utility (text-color, text-muted-color, text-primary, bg-surface-0, bg-surface-100, border-surface) OR set severity on a PrimeVue component and let the preset handle color.
| Raw ramp | Semantic / severity replacement |
|---|---|
text-red-500 | text-danger (or Tag :severity="danger" if it's a status) |
text-green-600 | text-success |
text-amber-600 / text-yellow-600 | text-warn |
text-blue-500 / text-sky-500 | text-info |
text-purple-500 | text-primary |
bg-red-100 | bg-danger/10 (or a tinted card surface) |
text-black-900 / text-gray-900 | text-color |
text-gray-500 | text-muted-color |
border-gray-200 | border-surface |
2. Hex literals in source
Pattern:
#[0-9a-fA-F]{3,8}(?![0-9a-fA-F])Where to ignore: Any file under design-system/ (canonical sources + generated outputs).
Where to flag: Everywhere else — .vue, .js, .ts, scoped .scss, mock data, comments-that-look-like-code.
Fix: Use the token. If the value isn't in the token system, that's a red flag — the preset should already have a token for it (--p-primary-500, --p-text-color, etc.). Add to DESIGN.md only if genuinely missing. For brand color outside PrimeVue, use var(--vendor-color) or useOnRampBrand.
3. Token vars stored as data
Pattern:
['"](var\(--p-[^)]+\))['"]Why bad: Color belongs in the component, not in data. Mock data carrying var(--p-primary-500) as a string couples data to a specific palette position. The data should carry severity codes ('danger', 'warn', 'success', etc.); components map severity → color at render time. The same rule applies to hardcoded hex in mock data — { color: '#7c5cff' } is forbidden.
Fix:
// BAD
{ status: 'risk', color: 'var(--p-red-500)' }
// GOOD
{ status: 'danger' } // component reads `status` and renders <Tag :severity="status">4. Inline color in :style
Pattern:
:style="[^"]*?(color|background|background-color|border-color|fill|stroke):Why bad: Should be a utility class or severity prop. Inline styles escape the layered cascade and can't respond to theme changes.
Acceptable exception: Setting a --p-* token via :style when the value is genuinely dynamic and can't be a class (rare). Must be a token, never a literal.
Fix: Class binding (:class="[isError && 'text-danger']") or a severity prop.
5. !important utilities
Pattern:
class="[^"]*\B!\w+(?:-\w+)*\bMatches class="… !px-8 …" etc.
Why bad: The layered model means utilities already override PrimeVue components without !important. Reaching for it signals a broken understanding of the cascade — usually pasting !important to "make the override stick" when the real fix is using the right selector or removing a conflicting rule.
Fix: Remove the !. If the override truly doesn't apply, the underlying problem is elsewhere (a scoped rule, an unlayered base reset) — fix that.
6. .p-* scoped CSS for color
Pattern (in a scoped <style> block):
:deep\(\s*\.p-[^)]+\)[^{]*\{[^}]*?(color|background|border-color|fill|stroke)\s*:OR (without :deep):
\.p-[\w-]+[^{]*\{[^}]*?(color|background|border-color|fill|stroke)\s*:Why bad: PrimeVue color belongs in the preset (or severity props). Touching .p-* selectors to set color in a single component creates drift the preset can't track, and is unreliable across PrimeVue upgrades.
Fix: Move color decisions to:
- The active preset (
app/ui-organization/theme/preset.js) if it's an app-wide deviation - A
severityprop on the PrimeVue component if it's a per-instance decision --p-*tokens via class names, not via:deep()
7. .p-* scoped CSS for layout without justification
Pattern: Same as #6 but matching layout properties (display, flex, grid, padding, margin, height, width).
Why partially bad: Sometimes legitimate (e.g. flexing a .p-card-body to fill height). Must be inline-commented to explain why utilities can't express the same thing.
Flag if: No comment is present, or the comment doesn't justify why a utility wouldn't work.
8. :pt / :dt as default
Pattern:
:pt=:dt=Why bad: Passthrough is for named, commented escape hatches for specific gaps the preset doesn't fill. Used as a default it bypasses the preset entirely.
Flag if: Used without an adjacent comment naming the gap (e.g. <!-- escape-hatch: on-surface-ground hover invert -->).
9. v3 component names
Pattern (in template imports OR template tags):
\b(Dropdown|Calendar|InputSwitch|OverlayPanel|Sidebar|TabView)\bWhy bad: Stale API names that import a different (or non-existent) component in v4.
Fix: The v3→v4 name map is canonical in .claude/rules/ui-design-system.md ("Stack & PrimeVue v4 names") — Select, DatePicker, ToggleSwitch, Popover, Drawer, Tabs+TabList+Tab+TabPanels+TabPanel.
10. Severity vocab mismatch
Pattern: Any severity="amber", severity="yellow", severity="orange", severity="red", severity="green", severity="blue" — color names where a meaning should be.
Why bad: The preset binds severities to palettes; passing a palette name where a meaning is expected silently misses the binding, and locks in a color that won't theme correctly.
Fix: Map color → meaning per the canonical severity vocabulary in .claude/rules/ui-design-system.md ("Severity is meaning, not intensity"):
amber/yellow/orange→warnred→dangergreen→successblue/sky→infopurple→primary
11. Color-only severity signals
Pattern (visual inspection, not strictly grep):
▲/▼characters withtext-red-*/text-green-*and no sr-only label- Status communicated by background color tint and no text
- Progress bar segmented by color but no aria-label
Why bad: WCAG 1.4.1 — color cannot be the only carrier of meaning.
Fix: Add <span class="sr-only">NaN</span> next to the visual signal, OR add aria-label to the containing element.
12. role="button" without keyboard handlers
Pattern:
role="button"[^>]*tabindexThen check if the same element has @keydown.enter AND @keydown.space.
Why bad: Screen readers and keyboard-only users can focus it but can't activate it.
Fix: Convert the element to a real <button> (preferred), OR add the two key handlers.
13. Hand-rolled SVG charts
Pattern (visual): A .vue file with <svg> blocks containing <path d="M…"> for trend/spark/multi-line visualizations.
Why bad: PrimeVue ships Chart (built on Chart.js) with proper theming, tooltips, and accessibility. Hand-rolled SVGs lack all of these.
Fix: Replace with <Chart type="line" :data="…" :options="…" />. The options blob can be templated for sparklines (no axes, single-line, small height).
14. Custom card shells
Pattern (visual): A .vue file with a custom <div> shell that has a header row, body, and styling consistent with a card pattern, but doesn't use <Card> from PrimeVue.
Why bad: Custom shells don't inherit the preset's card styling, can't respond to theme changes, and re-implement what PrimeVue already provides.
Fix: Use <Card> and compose the header into the #title / #header slot and the body into #content. For a KPI/stat tile, compose the tinted-icon-badge pattern from conventions.md §4 inside the Card. (Shared composites wrap this — components/composites/stats/StatCard.vue and composites/kpi/KpiTile.vue; reach for those before composing inline per §4.)
15. Exploration scheme selectors
Pattern:
\.x-(light|dark)\b|html\.x-Why bad: .x-light / .x-dark were the temporary classes of the visual-design-exploration overlay. The schemes shipped as the real theme: light is the default and dark rides the production .or-dark selector (darkModeSelector in the PrimeVue config), with scheme semantics owned by app/ui-organization/theme/preset.js. Any .x-* selector is dead code that silently never matches.
Fix: Express scheme-specific values through preset colorScheme tokens, light-dark(), or an .or-dark ancestor selector (org app) — see .claude/rules/ui-design-system.md.