Files
sim/scripts/check-bare-icons.ts
T
ad19f7fc40 improvement(landing): refine hero and mothership visuals (#5181)
* stash

* feat(landing): mothership feature stages + pre-footer CTA

Tell-then-show landing: the Mothership section defines the five capabilities
(Mothership · Pod · Formation · Dispatch · Return); the Features section now
shows each as a real Sim UI callout floating over a static, edge-faded platform
backdrop (Linear's "callout over a faded platform" pattern).

- FeatureStage template: copy + masked static LandingPreview + elevated callout
- LandingPreview: static autoplay=false snapshots with per-stage view/workflowId
- Callouts: Mothership chat, model picker, parallel-agents Formation graph,
  deploy targets, logs table
- Pre-footer CTA set over the Mothership render; removed the old capabilities grid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): reusable platform-page + solutions-page layouts and routes

Add config-driven, padding-safe layouts consumed by route pages:
- platform-page: hero (shared CTA) + centered logos + N card rows (3|4),
  JSON-LD, single <h1>, server-only; Workflows route as first consumer.
- solutions-page: structural mirror (kept separate to diverge later);
  IT, Engineering, Finance, Compliance, HR routes under /solutions.
- Hoist shared LandingShell/HeroCta/Logos to components/ (top-level =
  shared); refactor hero to consume them.
- Restructure all of (landing) to the workspace folder-per-component
  convention (each component in its own folder + index.ts barrel).

* refactor(landing): convert hero-visual CSS-module keyframes to Tailwind

Move the hero-visual + stage-home keyframe animations out of CSS modules
into tailwind.config (matching the existing dash-animation pattern) and
delete both module.css files. Components now use animate-hero-* utilities
+ arbitrary properties for the per-element delays, SVG stroke draw, and
gradient shimmer; reduced-motion preserved via motion-reduce: variants.
Upgrade the shimmer's hardcoded #b4b4b4 to the --text-subtle token.

brand-tokens.module.css is intentionally kept: it reassigns --surface-*/
--text-* token VALUES via a doubled-class selector for specificity over
.light, which Tailwind utilities cannot express.

* refactor(landing): move brand palette from CSS module into LandingShell

Replace brand-tokens.module.css with a BRAND_TOKENS constant of Tailwind
arbitrary-property utilities applied on the LandingShell wrapper, so the
brand hex lives in the component, not a stylesheet. They emit in the
utilities layer and override .light (@layer base) by cascade order —
verified the brand --text-primary (#121212) wins over .light (#1a1a1a).
No more .module.css files remain in the landing.

* chore(landing): remove Testimonials from the home page for now

Drop <Testimonials /> from the landing composition (component kept for
re-adding later).

* feat(landing): hero send→loader→workflow animation + landing WIP

Hero visual: clicking send zooms into the button, morphs the disc into the
gooey thinking loader (held, then cycling), slides it straight across to a
phrase indicator with the camera following (no zoom-out), then zooms back out
as the reply types and the chat morphs into the GitHub→Agent→Jira workflow.
The chat card holds a fixed size through the zoomed scene and the greeting
reserves its space, so nothing drifts; the user bubble reveals only on
zoom-out. Loader ink tweens dark→gradient via the thinking-loader
stop-color/flood-color transition.

Also folds in in-progress landing work: knowledge + integrations feature
callouts, CTA chat, mothership + line-glyph, wordmark tweak; removes the
ethos and testimonials sections.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): responsive pass for iPad + mobile

Make the landing page fully responsive while keeping the desktop layout
byte-identical (desktop classes stay the unprefixed baseline; smaller
screens layer max-* overrides on top).

- Navbar: hide desktop clusters below lg, add MobileNav hamburger sheet
  (scroll-lock, Escape/tap close, reduced-motion aware)
- Hero: collapse the absolute split (visual + logos) to a stacked column
  below xl so iPad-landscape avoids the headline/visual collision
- Mothership: 4-col grid steps to 2 (tablet) then 1 (phone)
- Features: drop the floating callout below md, show the un-masked
  backdrop preview full-width
- CTA + Footer: scale type/padding; footer 7-col steps to 3 then 2
- Document the breakpoint strategy in the landing CLAUDE.md

Also includes the in-progress mothership goo/iso brand marks and the
marks-lab preview route the section depends on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): align hero visual panel to text + logos extent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(landing): delay hero user bubble until card finishes expanding

The grey user bubble's fade-in raced the card's upward grow on send.
Hold the bubble's reveal until after the parent-driven grow settles so
the card expands fully before the bubble appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(landing): isolate new landing — remove dead old-folder code + --landing-* coupling

- Delete dead (landing) auth-modal (a duplicate of (home)'s, still on the old
  --landing-* / dark tokens) — its removal drops the new landing's last styling
  tie to the old landing.
- Delete 5 merge-orphaned, zero-consumer callouts (deploy-callout,
  mothership-chat-callout, mothership-chat-preview, workflow-graph-preview,
  model-picker-preview).
- Relocate the one live preview (logs-table-preview) into its consumer
  features/components/ + add a barrel; dissolve the owner-less feature-callouts/ shell.
- Fix stale --landing-bg-surface reference in landing-preview-mount.

(landing) now has zero (home) imports and zero --landing-* token usage.

* refactor(landing): token-map hex, fix a11y/SEO, align structure

Styling (within (landing)):
- Replace ~90 hardcoded hex colors with the in-scope brand tokens they already
  equal (--surface-*/--text-*/--border*); divider edges -> --border, field/card
  edges -> --border-1. Delete the redundant C color-palette mirrors in the
  landing-preview home/sidebar and route them through tokens.
- Convert static inline SVG styles (display:block/outline:none) to Tailwind.
- 6 un-tokenizable hexes remain (dark send-button fills, status-green dot) —
  no brand token exists; left as-is.

a11y / SEO:
- Decorative mothership goo/iso marks: role='img'+aria-label -> aria-hidden.
- Preview chrome titles <h1> -> <span> (kills duplicate client-only H1s).
- sitemap.ts: add /workflows and the five /solutions/* routes.

Structure:
- Folder the bare logo-mark/mobile-nav leaves + barrels; complete the navbar
  components barrel and consolidate navbar.tsx to a single barrel import.

* style(landing): restore taller hero panel with border-shadow chip chrome

- Revert the right visual panel to the previous full-height framing
  (top-8 bottom-8) — hero text (pt-[112px]) and the logos panel are untouched,
  so their positions and spacing are unchanged.
- Apply the canonical border-shadow chip surface: --surface-2 fill + the shared
  chipBorderShadowRing (1px hairline ring + soft drop shadow) from emcn, the
  documented chrome for a landing media panel.

* feat(landing): swap Volvo for thinkproject and reposition hero logos

- Replace Volvo with the thinkproject wordmark (official SVG, tagline/descriptor
  cropped out, all paths unified to --text-primary #1a1a1a; aspect 6.01).
- Reorder the shared 6-logo set so the 3x2 hero grid reads: Rivian|VW (top-left),
  eXp Realty (top-center), Russell (top-right); Artie (bottom-left),
  thinkproject (bottom-center), Mobile Health (bottom-right).
- Enlarge Rivian|VW a touch (height 15 -> 17, same aspect).
- eXp Realty, Artie, Russell, Mobile Health, Rivian|VW all retained.

* style(landing): size hero description with the type scale (text-lg)

Replace the arbitrary text-[20px]/text-[16px] on the hero description with named
scale tokens — text-lg (18px) desktop, text-md (16px) on phones — a touch smaller
and the canonical lead size (1.2x the platform's 15px base).

* style(landing): hero headline "for AI automations" with break after "agent"

Replace "solving automations" with the higher-intent "AI automations" and move
the line break after "agent" so "for AI automations." sits on the second line.

* style(landing): unify CTA radius and box hero logos in cards

- HeroCta email bar: rounded-[13px] -> rounded-lg, so the bar, the inset
  Book-a-demo chip, the Sign-up chip, and the navbar chips all share one radius.
- Hero logos: box each wordmark in a bordered --surface-1 card (platform card
  chrome: rounded-lg + --border-1, 100px tall) on a responsive 3-up grid
  (2-up on phones) at a consistent gap-5 rhythm. Wide marks scale to fit
  (max-w-full h-auto). The platform/solutions 'row' layout stays bare wordmarks.

* style(landing): concentric CTA bar radius + tighter logo cards

- HeroCta email bar back to rounded-[13px] (= inner chip 8px + ~5px inset) so the
  Book-a-demo chip's right corners nest concentrically inside the bar.
- Logo cards: smaller and tighter — h-20 (80px), px-4, gap-3 (12px, the product
  UI card-grid rhythm).

* style(landing): restore 100px logo cards, scale icons down 15%

The 80px cards read too wide-for-their-height. Restore h-[100px] (keeping the
tighter gap-3/px-4) and instead shrink the wordmarks to 0.85x their optical size
in the grid via GRID_ICON_SCALE — row layout unchanged.

* style(landing): match sign-up radius to email bar + shrink logo icons

- Sign-up chip overridden to the email bar's rounded-[13px], so the two hero CTAs
  share one corner radius.
- Logo icons: GRID_ICON_SCALE 0.85 -> 0.65 and card padding px-4 -> px-2; card
  dimensions (h-[100px], gap-3) unchanged.

* style(landing): shrink hero logo cards

Cards read massive — too tall (100px) and stretched to fill the panel. Drop to
h-16 (64px), cap width at w-[150px], and make the grid w-fit so it hugs the cards
instead of stretching. gap-3 and the 0.65 icon scale unchanged.

* style(landing): upscale hero logo cards ~25%

Cards read too small. Bump all dimensions: h-16->h-20 (80px), w-[150px]->w-[180px],
px-2->px-3, and icon scale 0.65->0.8. Grid stays content-hugging at gap-3.

* style(landing): taller logo cards, larger icons, reorder top row

- Card height h-20 -> h-[88px] (width w-[180px] unchanged), icon scale 0.8 -> 0.85.
- Top row reordered: eXp (left), Russell (center), Rivian|VW (right).

* style(landing): more card height, swap top-row Rivian/eXp back

- Card height h-[88px] -> h-24 (96px); width unchanged.
- Top row: Rivian|VW (left), Russell (center), eXp (right).

* feat(landing): add "Trusted by technical teams at" label above hero logos

Top-left, gap-3 above the logo grid (matching the grid rhythm); text-sm (navbar
text size) in --text-muted (the label token).

* style(landing): recolor logos to --text-body, match label gap to hero rhythm

- Recolor all six customer logo SVGs to #3b3b3b (--text-body light value), so they
  match the Sim navbar wordmark's color. Landing is light-only, so the hardcoded
  value always equals var(--text-body).
- Trusted-by label gap gap-3 -> gap-[22px] (the hero's description->CTA spacing).

* style(landing): scale hero CTA down a hair, drop radius to the nav chip's

Sign-up read too round. Take the bar + Sign-up to h-[40px] / rounded-lg (8px, the
navbar chip radius), and keep the inset Book-a-demo concentric: h-[2em] + rounded
(4px) with a 4px inset (8 = 4 + 4).

* style(landing): round Book-a-demo to rounded-md to match the bar curve

rounded (4px) read too square next to the bar's rounded-lg (8px). Bump to
rounded-md (6px) — echoes the bar's curvature, still inside the 4px inset.

* style(landing): match Book-a-demo proportions to the navbar chip

Restore h-[2.143em] (the chip's 30/14 height ratio); with px-[0.571em] (its 8/14
padding ratio) and the 16px label, Book-a-demo now shares the navbar chip's exact
height/padding/text proportions.

* style(landing): equal inset around Book-a-demo (h-[30px])

Button was h-[2.143em] (34.3px) -> only ~1.9px top/bottom vs 4px right inside the
bar's 38px inner box (40px minus the 1px border). Drop to h-[30px] (the nav chip
height) so it centers to an equal 4px inset on top, bottom, and right.

* style(landing): enlarge Book-a-demo to h-[32px], tighten inset to 3px

h-[30px] read too small/airy in the bar. Bump to h-[32px] and pr-[4px] -> pr-[3px]
so the inset is an equal, snugger 3px on top, bottom, and right.

* style(landing): lift hero logos off the bottom again (pb-20)

Restore the 80px bottom padding so the logos rest 112px above the section bottom
(mirroring the hero text's 112px top) instead of sitting flush with the visual
panel's bottom. max-xl:pb-0 keeps the stacked layout tight.

* improvement(landing): refine hero and mothership visuals

* fix(landing): cap hero fold height so it doesn't stretch on huge monitors

The section was min-h-[calc(100vh-62px)], so on very tall displays both absolute
panels (top-8 bottom-8) stretched — the visual panel grew gigantic and the
bottom-anchored logos sank to the very bottom. Cap the fold at 960px via
h-[min(calc(100vh-62px),960px)] (min-height can't be capped by max-height): the
whole hero stops growing, panels/logos stay proportioned like a large laptop, and
the next section just starts below. Laptops (<=16in) are unaffected; max-xl:h-auto
keeps the stacked layout below xl.

* refactor(landing): session cleanup — DRY CTA label, drop dead grayscale

Final tidy after this session's hero/CTA/logo iteration:
- hero-cta: extract the duplicated 16px label knob (px-[0.571em] + text-[16px] +
  font-size:inherit) into a single CTA_LABEL constant, matching the 'single knob'
  the TSDoc already describes — used by both Book-a-demo and Sign-up.
- logos: remove the grayscale filter (now a no-op — all wordmarks were recolored
  to a single #3b3b3b), inline the single-use LOGO_GAP_X, and flatten the nested
  cn() into plain layout ternaries (dropping the now-unused cn import).

* improvement(landing): animate mothership illustrations

* style(landing): solid-ink branding + hero cursor/loader polish

Branding: drop the bespoke BRAND_TOKENS palette and bottom-reveal from
LandingShell (use the platform's own light tokens); re-ink the wordmark,
logo-mark, and hero loader from the gradient+glow to a solid --text-body
so the marks read as one ink with the nav text. Add a `shimmer` prop to
ThinkingLoader for a static --text-body label, and stroke the squeeze
arcs with the shared gradient.

Hero visual: the cursor now enters from below the field and chases the
send button live through the zoom (retimed beats, no arrive-then-wait);
the greeting fades in gently instead of shimmer-revealing; the click
ring becomes a press-dip (hero-cursor-press replaces hero-click-ring and
hero-greeting-reveal). Extract BlockHandles so the morphed GitHub card
carries a real edge handle in scene space; seed the compose card at its
true height; pop the sent bubble in immediately.

* improvement(landing): update feature iso-marks to perfected geometry

Re-author the four Mothership iso-mark illustrations (Integrate, Ingest,
Build, Monitor) on the refined isometric geometry, keeping the existing
animation vocabulary intact: hover line-draw plus per-mark auto-motion
(integrate float, ingest pulse, monitor panel-separate, build grid-flow).
Map the raw exports onto the shared token palette/line weight for
consistency and tune per-mark sizes for one optical weight.

Build is now pure CSS (grid-flow replaces the RAF wave), so it drops
'use client' and renders as a server component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(landing): add pricing, privacy, terms, and changelog pages

- New public /pricing page: Free/Pro/Max/Enterprise cards with the full
  comparison breakdown transposed from shared upgrade data + JSON-LD; prices,
  CTAs, and features derive from shared billing constants so they can't drift
  from the in-app upgrade page.
- Migrate /privacy, /terms, and /changelog into the (landing) route group via a
  shared prose-page system (single source of truth for legal/prose chrome).
- Landing polish: solid-ink iso-mark illustrations + footer/cta/features/
  mothership spacing and token cleanups; sitemap adds /pricing.
- Audit pass: crawlable ChipLink CTAs, correct heading hierarchy, structured-data
  featureList derived from the visible comparison data, legal plan name Team->Max.

* large edits across landing finalization

* feat(auth): port OAuth-only signup + Microsoft provider from staging

Align auth-page logic with origin/staging (PR #5073) while keeping the
new chip-styled UI:

- Add Microsoft as a better-auth social sign-in provider (auth.ts) and
  surface it through the OAuth provider checker, providers API + contract,
  login/signup forms, SocialLoginButtons, and the landing auth modal.
- Gate email/password signup behind the emailSignupEnabled server flag
  (DISABLE_EMAIL_SIGNUP) so signup becomes OAuth-only when configured.
- Add DISABLE_MICROSOFT_AUTH / DISABLE_EMAIL_SIGNUP env + feature flags.

* fix(icons): render brand icons legibly when bare and on light tiles (#5292)

Monochrome brand icons hardcoded a single white or black fill matched to
their colored tile, so they vanished when rendered bare on the home
Suggested actions list (white-on-white in light mode, black-on-black in
dark mode). Convert those marks to currentColor so they adapt to context,
and make tile foregrounds contrast-aware via getTileIconColorClass instead
of a hardcoded text-white.

Also centralize all color math in apps/sim/lib/colors (perceived brightness,
hex/rgb/hsl conversion, contrast-text) and route every consumer through it:
the bare-icon audit, block tiles, logs trace view, whitelabeling theming,
workspace presence, and the PPTX renderer no longer carry duplicate copies.

Adds a bare-icon CI audit (scripts/check-bare-icons.ts) and authoring guidance.

---------

Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
Co-authored-by: andresdjasso <andresdjasso@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Waleed <walif6@gmail.com>
2026-06-30 13:50:04 -07:00

147 lines
5.7 KiB
TypeScript

#!/usr/bin/env bun
/**
* Audits brand icons that render "bare" (without their colored tile) for
* theme-safety. The suggested-actions surface and other bare contexts draw a
* block's icon on a neutral page in both light and dark mode. An icon whose
* SVG hardcodes only near-white fills vanishes on a light page; one that
* hardcodes only near-black fills vanishes on a dark page. The fix is to draw
* monochrome marks with `fill='currentColor'` (which adapts via the
* theme-aware foreground) and reserve hardcoded fills for genuinely
* multi-color brand logos.
*
* Scope: blocks that contribute suggested-action prompt rows — i.e. blocks
* whose meta defines `templates`. New integrations land here, so this is where
* regressions are caught. Multi-color icons and `currentColor` icons pass.
*
* Each block's main `icon:` AND every template's own `icon:` is audited (a
* template may reuse another block's brand icon). Only icons imported from
* `@/components/icons` are checked — that is the only module this script can
* resolve; `@sim/emcn/icons` are design-system line icons drawn with
* `currentColor` and are safe by construction, so they are intentionally
* skipped.
*
* Limitation: this catches purely-monochrome icons (only near-white or only
* near-black fills). It cannot catch an icon whose large primary shape is
* white but which also has a small vivid accent (the accent clears the
* heuristic) — that needs a visual light+dark check. Always eyeball new icons
* on the suggested-actions surface in both themes.
*
* Run: `bun run scripts/check-bare-icons.ts`
*/
import { readdir, readFile } from 'node:fs/promises'
import path from 'node:path'
import { perceivedBrightness } from '../apps/sim/lib/colors'
const ROOT = path.resolve(import.meta.dir, '..')
const BLOCKS_DIR = path.join(ROOT, 'apps/sim/blocks/blocks')
const ICONS_FILE = path.join(ROOT, 'apps/sim/components/icons.tsx')
const isNearWhite = (c: string) => {
const b = perceivedBrightness(c)
return b !== null && b > 0.9
}
const isNearBlack = (c: string) => {
const b = perceivedBrightness(c)
return b !== null && b < 0.1
}
/** Extract each exported icon's source body, keyed by component name. */
function indexIconBodies(src: string): Map<string, string> {
const bodies = new Map<string, string>()
const starts: Array<{ name: string; index: number }> = []
for (const m of src.matchAll(/export (?:function|const) (\w+)\s*[=(]/g)) {
starts.push({ name: m[1], index: m.index })
}
for (let i = 0; i < starts.length; i++) {
const end = i + 1 < starts.length ? starts[i + 1].index : src.length
bodies.set(starts[i].name, src.slice(starts[i].index, end))
}
return bodies
}
interface Hazard {
block: string
icon: string
kind: 'light' | 'dark'
detail: string
}
function analyzeIcon(body: string): { hazard: 'light' | 'dark' | null; detail: string } {
if (/currentColor/.test(body)) return { hazard: null, detail: 'uses currentColor' }
if (/url\(#|<stop|inearGradient|adialGradient/.test(body))
return { hazard: null, detail: 'gradient fill' }
const colors: string[] = []
for (const m of body.matchAll(/(?:fill|stroke)=(?:'([^']*)'|"([^"]*)")/g)) {
const v = (m[1] ?? m[2] ?? '').trim()
if (v && v.toLowerCase() !== 'none') colors.push(v)
}
const literal = colors.filter((c) => c.toLowerCase() !== 'currentcolor')
if (literal.length === 0) return { hazard: null, detail: 'no literal fills' }
const vivid = literal.filter((c) => !isNearWhite(c) && !isNearBlack(c))
if (vivid.length > 0) return { hazard: null, detail: `multi-color (${vivid[0]})` }
if (literal.every(isNearWhite))
return { hazard: 'light', detail: `only near-white fills (${literal.join(', ')})` }
if (literal.every(isNearBlack))
return { hazard: 'dark', detail: `only near-black fills (${literal.join(', ')})` }
return { hazard: null, detail: 'mixed' }
}
async function main() {
const iconsSrc = await readFile(ICONS_FILE, 'utf8')
const iconBodies = indexIconBodies(iconsSrc)
const blockFiles = (await readdir(BLOCKS_DIR)).filter((f) => f.endsWith('.ts'))
const hazards: Hazard[] = []
const seen = new Set<string>()
for (const file of blockFiles) {
const src = await readFile(path.join(BLOCKS_DIR, file), 'utf8')
if (!/\btemplates:\s*\[/.test(src)) continue
const brandIcons = new Set<string>()
for (const m of src.matchAll(
/import\s*(?:type\s*)?{([^}]*)}\s*from\s*'@\/components\/icons'/g
)) {
for (const name of m[1].split(',')) {
const trimmed = name.trim()
if (trimmed) brandIcons.add(trimmed)
}
}
for (const m of src.matchAll(/\bicon:\s*(\w+)/g)) {
const iconName = m[1]
if (!brandIcons.has(iconName)) continue
const key = `${file}:${iconName}`
if (seen.has(key)) continue
seen.add(key)
const body = iconBodies.get(iconName)
if (!body) continue
const { hazard, detail } = analyzeIcon(body)
if (hazard) {
hazards.push({ block: file.replace('.ts', ''), icon: iconName, kind: hazard, detail })
}
}
}
if (hazards.length === 0) {
console.log('✓ All suggested-action brand icons render safely bare in light and dark mode.')
process.exit(0)
}
console.error(`\nFound ${hazards.length} bare-icon hazard(s):\n`)
for (const h of hazards) {
const mode = h.kind === 'light' ? 'invisible on LIGHT pages' : 'invisible on DARK pages'
console.error(` ${h.block} (${h.icon}) — ${mode}`)
console.error(` ${h.detail}`)
console.error(
` Fix: draw the monochrome shape with fill='currentColor' in components/icons.tsx`
)
console.error(
` so it adapts to the theme bare and to the tile foreground (getTileIconColorClass).\n`
)
}
process.exit(1)
}
main()