Styling Guide
All styles for the Lens frontend live in a single CSS file: foundation/static/main/src/foundation.css (9000+ lines). The app does not use CSS modules, styled-components, or any CSS-in-JS solution.
Single-File CSS Architecture
Section titled “Single-File CSS Architecture”foundation.css├── Status spinner, tooltips, toolbar helpers (lines 1-150)├── Quick filter bar, rollup styles (lines 150-180)├── Sync agent section (lines 180-300)├── Inline editors (cell-editor-*, picker-*) (lines 300-600)├── AG Grid theme overrides (.ag-theme-alpine) (lines 1000-1800)├── Gantt chart (lines 3000-4000)├── Inspector panel & accordion sections (lines 4000-4500)├── Lens cards, lens menu (lines 5000-6000)├── Column header menus (lines 7500-8200)├── Pipeline studio (lines 8200-8500)├── Onboarding, templates, import (lines 8500-9000)└── Inline create panel, various features (lines 9000+)Atlassian Design System Tokens
Section titled “Atlassian Design System Tokens”All colors use ADS (Atlassian Design System) CSS custom properties with hex fallbacks:
/* Correct — always include a hex fallback */color: var(--ds-text, #172B4D);background: var(--ds-surface-sunken, #F7F8F9);border-color: var(--ds-border, #DFE1E6);
/* Wrong — never use bare hex values for standard colors */color: #172B4D;The fallback ensures the app renders correctly when ADS tokens are unavailable (e.g., in test environments or if theme injection fails).
Common Token Mapping
Section titled “Common Token Mapping”| Token | Purpose | Fallback |
|---|---|---|
--ds-text | Primary text | #172B4D |
--ds-text-subtle | Secondary text | #626F86 |
--ds-text-subtlest | Tertiary text | #626F86 |
--ds-text-inverse | Text on dark backgrounds | #FFFFFF |
--ds-surface | Default background | #FFFFFF |
--ds-surface-sunken | Recessed background | #F7F8F9 |
--ds-surface-hovered | Hover background | #EBECF0 |
--ds-background-selected | Selected row | #DEEBFF |
--ds-background-brand-bold | Primary action | #0052CC |
--ds-border | Default border | #DFE1E6 |
--ds-link | Links | #0052CC |
--ds-icon | Standard icons | #505F79 |
--ds-icon-subtle | Subtle icons | #6B778C |
--ds-icon-brand | Brand-colored icons | #0052CC |
Hardcoded Accent Colors
Section titled “Hardcoded Accent Colors”Certain semantic colors are hardcoded (not from ADS tokens) for specific feature meanings:
| Color | Hex | Usage |
|---|---|---|
| Purple | #6554C0 | Flex items, user avatar fallback backgrounds |
| Amber | #D97706 | Milestones |
| Green | #00875A | Success states, toggles ON |
| Red | #DE350B / #FF5630 | Danger states, destructive actions |
| Blue | #0052CC | Primary actions, links, focus rings |
Density Modes
Section titled “Density Modes”The app supports 5 density modes, controlled by a CSS class on the grid container:
| Mode | CSS Class | Row Height | Header Height |
|---|---|---|---|
| Comfortable | .density-comfortable | 44px | 34px |
| Standard | .density-standard | 36px | 32px |
| Compact | .density-compact | 28px | 28px |
| Supercompact | .density-supercompact | 22px | 22px |
| Micro | .density-micro | 16px | 16px |
Density is implemented via parent class selectors that override child component sizing:
/* Base (comfortable — the default) */.sa-section { padding: 12px 16px; }
/* Compact overrides */.density-compact .sa-section { padding: 2px 6px; }.density-compact .sa-row { font-size: 11px; padding: 1px 0; }
/* Standard overrides */.density-standard .quick-filter-bar { padding: 3px 10px; min-height: 28px; }Row heights also have a “with description” variant when stacked description subtitles are enabled:
| Mode | With Description |
|---|---|
| Comfortable | 62px |
| Standard | 54px |
| Compact | 42px |
| Supercompact | 32px |
| Micro | 16px (never shows descriptions) |
Rule: Every new component must render correctly at all 5 density levels. Test at comfortable, compact, and micro at minimum.
Inline Styles vs. CSS Classes
Section titled “Inline Styles vs. CSS Classes”CSS classes (in foundation.css) for:
- Static layout and positioning
- Theme-based colors using ADS tokens
- Density overrides
- Hover/focus/active states
- AG Grid theme customization
Inline styles for:
- Dynamic/computed values (indentation depth, progress bar width)
- Conditional formatting (user-defined background/text colors)
- Density-aware sizing that depends on runtime calculations
- Icon sizing that varies by context
// Correct — dynamic value needs inline style<div style={{ paddingLeft: depth * 24 }}>
// Correct — conditional formatting from user rules<div style={{ backgroundColor: rule.bgColor, color: rule.textColor }}>
// Wrong — static style should be a CSS class<div style={{ fontSize: 12, fontWeight: 600 }}>Typography Scale
Section titled “Typography Scale”| Size | Weight | Use |
|---|---|---|
| 11px | 600, uppercase, letter-spacing: 0.04em | Section headers in inspector |
| 12px | 400-600 | Labels, badges, form hints, metadata |
| 13px | 400 | Secondary body text, metadata |
| 14px | 400 | Primary body text, grid cells (comfortable density) |
| 16px | 600 | Page titles, card titles |
Font weight conventions:
- 600 — headers, labels, emphasized text
- 500 — subheadings
- 400 — body text
Menu & Dropdown Positioning
Section titled “Menu & Dropdown Positioning”All menus and dropdowns use position: fixed with viewport clamping — never position: absolute relative to a parent:
// Typical pattern in menu componentsfunction clampToViewport(rect: DOMRect): { top: number; left: number } { const { innerWidth, innerHeight } = window; let top = rect.bottom; let left = rect.left; // Clamp to viewport edges if (top + menuHeight > innerHeight) top = rect.top - menuHeight; if (left + menuWidth > innerWidth) left = innerWidth - menuWidth; return { top: Math.max(0, top), left: Math.max(0, left) };}Close behavior: Menus close on outside mousedown (not click) via document.addEventListener('mousedown', ...). Using mousedown prevents the dropdown from closing and then immediately reopening when the trigger button is clicked.
Standard dimensions:
- Width: 180px (column menus), varies for other menus
- Max height: 300px with
overflow-y: auto - Border radius: 8px
- Shadow:
box-shadow: 0 8px 16px rgba(9, 30, 66, 0.25)(elevation-3)
Transition Timing Standards
Section titled “Transition Timing Standards”| Duration | Easing | Use |
|---|---|---|
| 100ms | ease | Background color changes |
| 150ms | ease | Hover states, opacity reveals |
| 200ms | ease | Expand/collapse, panel open/close, chevron rotation |
| 250ms | cubic-bezier(0.4, 0, 0.2, 1) | Panel slide transitions |
/* Chevron rotation example */.chevron { transition: transform 200ms ease; }.chevron.expanded { transform: rotate(90deg); }
/* Hover example */.toolbar-btn { transition: background-color 100ms ease; }.toolbar-btn:hover { background-color: var(--ds-surface-hovered, #EBECF0); }AG Grid Custom Styling
Section titled “AG Grid Custom Styling”Theme Override Pattern
Section titled “Theme Override Pattern”The app uses AG Grid’s legacy CSS theme (ag-theme-alpine) with extensive CSS variable overrides:
.ag-theme-alpine { --ag-background-color: var(--ds-surface, #FFFFFF); --ag-header-background-color: var(--ds-surface-sunken, #FAFBFC); --ag-header-foreground-color: var(--ds-text-subtle, #626F86); --ag-selected-row-background-color: var(--ds-background-selected, #DEEBFF); --ag-border-color: var(--ds-border, #DFE1E6); --ag-row-hover-color: transparent !important; /* Disabled — see hover section */}AG Grid Hover System
Section titled “AG Grid Hover System”AG Grid has two independent hover mechanisms:
-
JS-based hover (
.ag-row-hoverclass): AG Grid calculates which row the mouse is over via Y-position math and adds.ag-row-hoverto that row’s DOM element. This fires even in empty viewport space below the last data row, creating phantom hover highlights. -
CSS-based hover (
.ag-row:hover): Standard CSS pseudo-class. Only fires when the mouse is actually over an.ag-rowDOM element.
The solution:
// In HierarchyGrid.tsx:<AgGridReact suppressRowHoverHighlight={true} // Disable JS-based hover (#1) theme="legacy" // Use CSS file themes/>/* In foundation.css: */
/* Kill any residual AG Grid hover coloring */--ag-row-hover-color: transparent !important;
/* Neutralize JS-based hover pseudo-element */.ag-theme-alpine .ag-row-hover:not(.ag-full-width-row)::before { background-color: transparent !important; background-image: none !important;}
/* Pure CSS hover — only fires on real data rows */.ag-theme-alpine .ag-row[row-id]:hover { background: var(--ds-surface-hovered, #F4F5F7);}The [row-id] attribute selector ensures hover only applies to rows that AG Grid has assigned data to, preventing highlights in empty viewport space.
Rule: Never re-enable AG Grid’s JS hover (suppressRowHoverHighlight={false}) or set --ag-row-hover-color to a non-transparent value.
Row Drag Handles
Section titled “Row Drag Handles”Drag handles are hidden by default and revealed on row hover via CSS:
.ag-theme-alpine .ag-row .ag-row-drag { opacity: 0; transition: opacity 0.15s ease;}.ag-theme-alpine .ag-row[row-id]:hover .ag-row-drag,.ag-theme-alpine .ag-row-selected .ag-row-drag { opacity: 1;}Selection Checkboxes
Section titled “Selection Checkboxes”Similarly, selection checkboxes are hidden until hover or selection:
.ag-theme-alpine .ag-row .ag-selection-checkbox { visibility: hidden;}.ag-theme-alpine .ag-row[row-id]:hover .ag-selection-checkbox,.ag-theme-alpine .ag-row-selected .ag-selection-checkbox { visibility: visible;}Editing State
Section titled “Editing State”During cell editing, drag handles and interactive elements are suppressed:
.ag-editing-active .ag-row[row-id]:hover .ag-row-drag { opacity: 0 !important;}Tooltip System
Section titled “Tooltip System”The app uses a custom tooltip component (components/common/Tooltip.tsx) instead of ADS tooltips. Tooltips use position: fixed and are viewport-clamped.
.foundation-tooltip { position: fixed; z-index: 6000; padding: 10px 12px; border-radius: 8px; background: rgba(23, 43, 77, 0.96); color: var(--ds-text-inverse, #FFFFFF); box-shadow: 0 12px 24px rgba(9, 30, 66, 0.2); transition: opacity 140ms ease, transform 140ms ease;}Tooltips support placement (top, bottom, left, right) with slide-in animations and optional arrow indicators.
Icon Conventions
Section titled “Icon Conventions”- All icons are inline SVGs — no icon libraries
- Standard
viewBox="0 0 24 24", sized 14-16px (density-aware) - Priority icons:
utils/priorityIcons.tsx(colored arrow SVGs) - Issue type icons:
utils/issueTypeIcons.tsx(colored category SVGs) - Chevrons: inline SVG with
rotate(90deg)when expanded, 200ms ease transition
Collapsible Sections
Section titled “Collapsible Sections”In the Inspector Panel
Section titled “In the Inspector Panel”Use AccordionSection with the following required props:
sectionId— unique ID for localStorage persistencetitle— section header textbadge— optional count badgedefaultExpanded— initial stateonToggle— callback for expand/collapse
Anywhere Else
Section titled “Anywhere Else”Use AccordionSection (same component) without inspector-specific props.
Button Conventions
Section titled “Button Conventions”Use @atlaskit/button/new with spacing="compact" in toolbars and panels:
import Button from '@atlaskit/button/new';
// Primary action — one per visible context<Button appearance="primary" spacing="compact">Create Lens</Button>
// Toolbar/header actions<Button appearance="subtle" spacing="compact">Options</Button>
// Destructive actions<Button appearance="danger" spacing="compact">Delete</Button>Overscroll Prevention
Section titled “Overscroll Prevention”html, body { overscroll-behavior-x: none;}Prevents browser back/forward navigation on horizontal trackpad swipes, which would navigate away from the Forge iframe.
Z-Index Layers
Section titled “Z-Index Layers”| Z-Index | Element |
|---|---|
| 6000 | Tooltips |
| 3000 | Announcement popup |
| 1000+ | Modal dialogs (via @atlaskit/modal-dialog) |
| 100-200 | Menus, dropdowns |
| 10 | Fixed headers |
| 0 | Grid content |