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.svg → hienergy_logo_black.svg
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.
app/assets/stylesheets/shared/_design_tokens.scss; start with
Color and
Typography.render_search_input_row, the
Filter toolbar, and the
Data table density.
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.
Montserrat everywhere. Headings are 600, body 400, monospace for
code / kbd / pre.
Body — 1rem. The quick brown fox jumps over the lazy dog.
Small — 0.875rem. Helper text, captions, meta.
Anchor example ·
Strong ·
Emphasized ·
inline-code ·
Cmd + K
def hello
puts "Monospace: SFMono-Regular, Menlo, Monaco"
end
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.
/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).
Navigation, sign-in, org JSON-LD logo, and light surfaces. Primary stable wordmark path.
/branding/hienergy-logo-black.svg
slug: hienergy-logo-black.svg → hienergy_logo_black.svg
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.svg → hienergy-logo-white.svg
Browser tab icon. Layouts use `favicon_link_tag` with the stable path via `public_brand_asset_path("favicon.ico")`.
slug: favicon.ico → hi.ico
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 / MCP chat face (offcanvas, workspace, message bubbles)
helper: image_path("dex_avatar.png")
world.svg
World map and geographic chart primitives when bundled through the asset pipeline
helper: image_path("world.svg")
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 |
Focused inputs get a purple-tinted ring matching the accent.
.card-header.bg-white keeps the header on-brand.
.border-0.shadow-sm for emphasis without an outline.
| Advertiser | Network | Status | EPC |
|---|---|---|---|
| Acme Running | Impact | Approved | $1.24 |
| Globex Fitness | Partnerize | Applied | $0.88 |
| Initech Outdoor | Pepperjam | Rejected | $0.12 |
| Umbrella Gear | CJ | Not applied | — |
$focus-ring-color is purple at 25% so keyboard focus
carries the accent without drowning the UI.
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.
Deals you create or import will appear here. Start by adding your first program.
Try a broader term or clear some filters.
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.
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).
Hairline chips replace loud Bootstrap badges on header meta, filter summaries, and inline tags. One dot, one short label, one border.
.advertiser-chip.filter-summary + .filter-chip.network-pillshared/_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)
Text only (show_logo: false)
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 |
One pattern: .input-group with form-control border-end-0,
optional clear + Stimulus search, and a single primary search button
(bi-search + visually-hidden label). Prefer
render_search_input_row (SearchFormHelper; variants
:full / :compact) → shared/search_input_row.
Admin lists use shared/search_filter_bar; tight spots use
search-form-navbar. Do not use input-group-text with a magnifier icon.
Stimulus targets for omnibox/autocomplete are declared on the field; the row matches the same visual chrome.
searchable_select_controller on a native form-select (or form-select-sm in dropdowns);
panel styling matches filter-dropdown-menu. Toolbars: Filter combobox +
admin/advertisers/single_filter_select. Tags: tag_autocomplete + shared/tag_search.
Inline search + selects + actions; only the primary is filled. List pages on
/changes, public deal lists, and similar screens.
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).
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.
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.
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.
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.
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.
Performance metrics over time.
The nav can show a trailing badge: count, and
each button supports a tooltip: for longer
descriptions.
Audit log of changes.
Conditionally rendered tabs should use hidden: !condition
on t.pane — never wrap the call in an
if block.
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 %>