Design System · v1

Crisp, techy, electric purple.

A living reference for the visual language of the app — typography, color, spacing, and components. White canvas, cool zinc neutrals, and a single accent that carries every primary CTA, link, and focus state.

Montserrat
Type family
#8B5CF6
Brand accent
Zinc
Neutral scale
0.375rem
Base radius
1.5
Line-height
Quick start
  • Tokens — palette and type live in app/assets/stylesheets/shared/_design_tokens.scss; start with Color and Typography.
  • Actions — one purple accent; patterns in Buttons and Forms.
  • List pages — use Search with render_search_input_row, the Filter toolbar, and the Data table density.
  • Deeper — comboboxes, chips, and tabs are linked in the table of contents.
01 · Color

Palette

One accent, one ink, one neutral scale. Semantic colors are present but used sparingly so the purple stays the only loud color on screen. Edit app/assets/stylesheets/shared/_design_tokens.scss and it cascades site-wide.

Electric purple
$brand-purple $primary #8B5CF6
Brand ink
$brand-ink #18181B

Accent ramp

Subtle
#F5F3FF
$brand-purple-subtle
Base
#8B5CF6
$brand-purple
Hover
#7C3AED
$brand-purple-hover
Active
#6D28D9
$brand-purple-active
Emphasis
#5B21B6
$brand-purple-emphasis

Neutral ramp — zinc

50
#FAFAFA
$ink-50
100
#F4F4F5
$ink-100
200
#E4E4E7
$ink-200 • border
300
#D4D4D8
$ink-300
400
#A1A1AA
$ink-400
500
#71717A
$ink-500
600
#52525B
$ink-600 • secondary
700
#3F3F46
$ink-700 • body
800
#27272A
$ink-800
900
#18181B
$ink-900

Semantic

Success
#22C55E
$success • green-500
Danger
#EF4444
$danger • red-500
Warning
#F59E0B
$warning • amber-500
Info
#06B6D4
$info • cyan-500
02 · Type

Typography

Montserrat everywhere. Headings are 600, body 400, monospace for code / kbd / pre.

Display — 3.5rem
display-4 · 600 · -0.02em

Heading 1 — 2.5rem

h1 · 600 · 1.2

Heading 2 — 2rem

h2 · 600 · 1.2

Heading 3 — 1.75rem

h3 · 600 · 1.2

Heading 4 — 1.5rem

h4 · 600 · 1.2
Heading 5 — 1.25rem
h5 · 600 · 1.2

Body — 1rem. The quick brown fox jumps over the lazy dog.

body · 400 · 1.5

Small — 0.875rem. Helper text, captions, meta.

small · 400 · text-body-secondary

Anchor example · Strong · Emphasized · inline-code · Cmd + K

inline elements
def hello
  puts "Monospace: SFMono-Regular, Menlo, Monaco"
end
pre · monospace
03 · Static assets

Logos, icons, and files

Stable, first-party URLs under /branding are served by BrandingController and listed in Branding::PublicAssets::SOURCE_BY_SLUG — the same set the MCP get_hienergy_design_system_guide tool returns for emails and partners. Pipeline images use Sprockets digests; the public web root has a few legacy and PWA files.

Stable /branding/* (email-safe, no digest)

Absolute URL on any environment: <%= request.base_url %> + path (or public_brand_asset_path(...) in views, Branding::PublicAssets.url_for in mailers/JSON-LD helpers).

Monochrome (dark on light)

Monochrome (dark on light)

Navigation, sign-in, org JSON-LD logo, and light surfaces. Primary stable wordmark path.

/branding/hienergy-logo-black.svg

slug: hienergy-logo-black.svghienergy_logo_black.svg

Reversed (light on dark)

Reversed (light on dark)

Hero, footer, mail header image, and dark or purple bands. First-party /branding URL in email HTML.

/branding/hienergy-logo-white.svg

slug: hienergy-logo-white.svghienergy-logo-white.svg

Favicon

Favicon

Browser tab icon. Layouts use `favicon_link_tag` with the stable path via `public_brand_asset_path("favicon.ico")`.

/branding/favicon.ico

slug: favicon.icohi.ico

Other images in app/assets/images (Sprockets)

Use image_tag "…" or asset_path in the app. URLs include a digest in production — do not use in email HTML.

dex_avatar.png

dex_avatar.png

Dex / MCP chat face (offcanvas, workspace, message bubbles)

helper: image_path("dex_avatar.png")

world.svg

world.svg

World map and geographic chart primitives when bundled through the asset pipeline

helper: image_path("world.svg")

Web root in public/

Served as-is; no Sprockets. Favicon handling should align with the branding section above for product UI.

Path Notes
/favicon.ico Fallback favicon; prefer `/branding/favicon.ico` for consistency with the design system
/icon.svg Generic icon asset in public (used where a root-relative icon is required)
/icon.png Bitmap icon in public
/apple-touch-icon.png Home-screen icon
/apple-touch-icon-precomposed.png Legacy iOS precomposed touch icon
04 · Action

Buttons

.btn-primary and .btn-outline-primary are bound to $brand-purple. Everything else uses Bootstrap's --bs-btn-* variables, so any .btn-* picks up the new tokens automatically. Every solid variant shares the same top-to-bottom lighten(base, 12%) → base gradient via the btn-gradient mixin in application.bootstrap.scss, so the whole button family reads as one system regardless of hue. Outline variants inherit the gradient on hover / active so they visibly snap into the family the moment they're engaged.

Solid
Outline
Sizes
States
Button group · dropdown
New-record button

Every "create a new record" CTA in the app — admin index pages, dashboards, tab toolbars, modal triggers — is an icon-only primary button with the bi-plus-lg glyph and a title + aria-label like "New agency". One glyph, one shape, everywhere.

← default, small, alt color
05 · Input

Forms

Focused inputs get a purple-tinted ring matching the accent.

Helper text goes here.
06 · Signal

Badges

Solid
Primary Secondary Success Danger Warning Info Light Dark
Pill · subtle
Pill Pill Pill Subtle pill
07 · Feedback

Alerts

08 · Surface

Cards & tables

Default card

#fff surface, $ink-200 border, 0.375rem radius, $box-shadow-sm.

Primary action
With header

.card-header.bg-white keeps the header on-brand.

Borderless + shadow

.border-0.shadow-sm for emphasis without an outline.

AdvertiserNetworkStatusEPC
Acme RunningImpactApproved$1.24
Globex FitnessPartnerizeApplied$0.88
Initech OutdoorPepperjamRejected$0.12
Umbrella GearCJNot applied
09 · Shape

Radii, elevation, focus

Radii

sm
0.25rem
base
0.375rem
lg
0.5rem
xl
0.75rem

Elevation

$box-shadow-sm Cards, tables.
$box-shadow Dropdowns, popovers.
$box-shadow-lg Modals, hero cards.

Focus ring

$focus-ring-color is purple at 25% so keyboard focus carries the accent without drowning the UI.
10 · States

Empty & loading

Use shared/empty_state and shared/loading_state anywhere a list, panel, or turbo frame can be empty or pending. One voice, one visual rhythm.

Empty state — card

No deals yet

Deals you create or import will appear here. Start by adding your first program.

Empty state — bare (inside a card)

No matches

Try a broader term or clear some filters.

Loading state — inline

Loading…

Loading state — block (panels, turbo frames)

Loading performance data…
11 · Scaffold

Page header & breadcrumbs

Every top-level page starts with render "shared/page_header". It ties breadcrumbs, avatar, title, subtitle, meta badges, and action buttons into one rhythm. Pass an actions_extra slot for dropdowns or custom chips.

12 · Metric

KPI tiles

The hairline KPI tile is the default metric pattern: colored dot, uppercase kicker, dark count, muted meta. Used on /changes, the publisher show page, and the advertiser show page (via .publisher-kpi-card / .recent-changes-kpi).

Applied
1,204
in the last 24 hours
Approved
318
in the last 24 hours
Rejected
47
in the last 24 hours
Stopped
9
in the last 24 hours
13 · Chip

Chips

Hairline chips replace loud Bootstrap badges on header meta, filter summaries, and inline tags. One dot, one short label, one border.

Meta chip — .advertiser-chip

Approved Applied Rejected 12 deals acme.test

Filter summary — .filter-summary + .filter-chip

Filters Network: Impact Status: Applied Publisher: Daily Mail 1,204 results

Network pill — .network-pill

Impact Partnerize Rakuten Avantlink

Advertiser list identity — shared/_advertiser_list_identity

Pass show_logo: false (default is true) when a row already shows a large logo nearby — for example the Advertiser column on All Deals — so the name and meta lines are not repeated twice with art.

With logo (default)

1MORE logo
1MORE
Impact Reshop

Text only (show_logo: false)

1MORE
Impact Reshop
14 · Data

Data table

Dense default: no grey thead bar, uppercase hairline kicker headers, 0.5rem vertical cell padding, purple-tinted row hover. This is the rhythm used on /changes and ports cleanly to any listing view.

Advertiser Network Status change When
Acme Running
Impact • PayPal Honey
Impact Applied Approved about 9 hours ago
Globex Fitness
Partnerize • Minty
Partnerize Rejected Approved about 9 hours ago
Initech Outdoor
Pepperjam • Minty
Pepperjam Approved Rejected about 9 hours ago
Umbrella Gear
Rakuten • Daily Mail
Rakuten Unknown Stopped about 9 hours ago
16 · Filter

Filter toolbar

Inline search + selects + actions; only the primary is filled. List pages on /changes, public deal lists, and similar screens.

Filter combobox (toolbar)

Single-value filter rows (admin index pages, deals#index, etc.) use admin/advertisers/single_filter_select: a field-shaped trigger (filter-combobox / filter-combobox__toggle) and a filter-dropdown-menu panel with search. Styles live in app/assets/stylesheets/components/_filter_combobox.scss. For a combobox not inside a filter bar, see Searchable combobox (Stimulus).

Filters
Country

Public sample deals (card + frame)

Sample Deals (and similar lists): white rounded-3 card, filter-stacking-context so comboboxes stack above the table, filter-toolbar--compact / --nested, single_filter_select in a row, min-w-0 on the search group, and clear_filters_link (with turbo_frame when the form targets a frame). Language toggles can use filter-toolbar__lang-scroll for horizontal scroll on small screens.

Filters

Field
Field
Field
Filters Network: Impact Status: Applied 42 results
17 · Segmented

Segmented control

A quiet pill-shaped toggle for mutually exclusive views (e.g. "All / People / List emails" on the contacts tab). Use when the options are short and switching is expected to be frequent; prefer tabs for larger sections.

18 · List

Contact list

Dense list rhythm for people rows: avatar + name, role · company subtitle, email/phone meta line, and an optional rating capsule. Rows hairline-divide and tint on hover. Used on the advertiser "Contacts" tab.

19 · Tabs

Tabs

Every tabbed surface in the app uses one universal system: render_tabs for the nav + content shell, and tab_header at the top of every pane body. Tabs have one canonical visual treatment.

Tabs
Default · show pages · dashboards · configuration

Overview

Short helper line under the title. Keep it to a single sentence.

Tab bodies should start with tab_header and then render their actual content — tables, charts, forms, whatever is needed.

Statistics

Performance metrics over time.

The nav can show a trailing badge: count, and each button supports a tooltip: for longer descriptions.

History

Audit log of changes.

Conditionally rendered tabs should use hidden: !condition on t.pane — never wrap the call in an if block.

Usage
Copy-paste starter · render_tabs + tab_header
<%= render_tabs(id: "advertiserTabs",
                data: { controller: "tab-url" }) do |t| %>
  <% t.pane "overview",
           title: "Overview",
           icon: "bi-info-circle",
           active: true,
           url_hash: "overview" do %>
    <%= tab_header title: "Overview",
                   icon: "bi-info-circle",
                   description: "Details and attributes." %>
    <%# ...pane body... %>
  <% end %>

  <% t.pane "stats",
           title: "Statistics",
           icon: "bi-bar-chart",
           hidden: !@has_stats,
           url_hash: "stats" do %>
    <%= tab_header title: "Statistics",
                   icon: "bi-bar-chart",
                   description: "Performance metrics over time." do %>
      <%= link_to "Export CSV", export_path, class: "btn btn-primary btn-sm" %>
    <% end %>
    <%# ...pane body... %>
  <% end %>
<% end %>