Skip to content

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-build self-checks generated output against it (.agents/skills/design-handoff-build/references/forbidden-patterns.md).
  • /design-handoff-eval audits against it (.agents/skills/design-handoff-eval/references/audit-procedure.md §3–4).
  • .claude/rules/ui-design-system.md states 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)\b

Why 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 rampSemantic / severity replacement
text-red-500text-danger (or Tag :severity="danger" if it's a status)
text-green-600text-success
text-amber-600 / text-yellow-600text-warn
text-blue-500 / text-sky-500text-info
text-purple-500text-primary
bg-red-100bg-danger/10 (or a tinted card surface)
text-black-900 / text-gray-900text-color
text-gray-500text-muted-color
border-gray-200border-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:

js
// 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+)*\b

Matches 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 severity prop 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)\b

Why 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/orangewarn
  • reddanger
  • greensuccess
  • blue/skyinfo
  • purpleprimary

11. Color-only severity signals

Pattern (visual inspection, not strictly grep):

  • / characters with text-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"[^>]*tabindex

Then 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.

Internal documentation — gated behind Cloudflare Access.