Files
zeekay 72b7ad6687 brand: refuse the upstream mark, not just its name
bin/debrand renames text. A brand is also geometry and a hex triple, and
neither is a name, so the rename walked straight past both: the upstream
wordmark sat on the OAuth consent screens, the toolbar error page and the
2FA template drawn as vector paths, and inside index.html as a data URI
that printed the logo into every visitor's browser console. `--check` was
green through all of it.

So the marks are now refused directly. check_marks() reads the three
upstream hues in every spelling they are written in here -- `#1d4aff`,
`%231D4AFF` inside a data URI, the 8-digit alpha form, and the decimal
`(245, 78, 0)` that a hex grep cannot see -- plus two path segments that
identify the wordmark even after its fills are recoloured, because
recolouring it does not debrand it. The hue match is bounded by non-hex on
both sides so the 64-char checksums in the lockfiles are not false
failures, with `%23` spelled out as the exception since it ends in a hex
digit. Exemptions go in CONTENT_ALLOWLIST with a reason, as they already
do for names; no second check script.

The decimal spelling immediately found four files no hex grep had
reported: two toolbar pills whose drop shadow was a fixed brand blue under
a var(--primary-3000) background, a clickmap overlay, and a
snapshot-history glow frozen at the old accent's value.

The tree it now refuses is clean. Decoration collapses to ink, reusing
what the fork already decided rather than inventing a hue: base.scss kept
the three brand slots and zeroed their saturation (#333 / #8f8f8f / #ccc),
and --data-color-1 is already #3d3d3d, so the quill token palette, the
charts fallback and the MCP palette converge on it instead of drifting
from it. Where the artwork itself was upstream's -- the 2FA template, the
MCP app logo, the OAuth region picker, the Stripe app icon and the five
billing plan badges -- it is replaced by the mark in
frontend/public/icons/safari-pinned-tab.svg.

Someone else's product mark keeps someone else's colours: the vendor logos
under frontend/public/services are untouched, the Hedgebox fixtures use
Hedgebox's own declared accent, and the livestream country palette stays
chromatic because it is data encoding, not decoration.

.stylelintignore gains the generated quill dist, which has never been
stylelint-clean and cannot be made so without editing generated output.
2026-08-08 16:13:57 -07:00

250 lines
6.1 KiB
Go

package tui
import (
"image/color"
"charm.land/bubbles/v2/help"
"charm.land/lipgloss/v2"
sharedpalette "github.com/insights/insights/phrocs/internal/palette"
"github.com/insights/insights/phrocs/internal/process"
)
// Plain Unicode, no embedded ANSI so they can be safely composed
// without resetting the enclosing background or foreground colour
const (
iconCharRunning = sharedpalette.IconRunning
iconCharPending = sharedpalette.IconPending
iconCharStopped = sharedpalette.IconStopped
iconCharDone = sharedpalette.IconDone
iconCharCrashed = sharedpalette.IconCrashed
iconCharStandby = sharedpalette.IconStandby
)
var (
colorYellow = sharedpalette.ColorYellow
colorBlue = sharedpalette.ColorBlue
colorGreen = sharedpalette.ColorGreen
colorRed = sharedpalette.ColorRed
colorBlack = sharedpalette.ColorBlack
colorWhite = sharedpalette.ColorWhite
colorBrightWhite = sharedpalette.ColorBrightWhite
colorBrightBlack = sharedpalette.ColorBrightBlack
colorBrightYellow = sharedpalette.ColorBrightYellow
selectionDark = sharedpalette.SelectionDark
selectionLight = sharedpalette.SelectionLight
brandYellow = sharedpalette.BrandYellow
brandGray = sharedpalette.BrandGray
brandRed = sharedpalette.BrandRed
brandBlack = sharedpalette.BrandBlack
)
// Outer width of the process list column (including border)
const sidebarWidth = 24
// Outer width of the container sidebar (including border)
const containerSidebarWidth = 24
const headerHeight = 1
const footerHeightShort = 3
const footerHeightFull = 5
const horizontalBorderCount = 4
var (
// Header
headerBrandStyle = lipgloss.NewStyle().
Bold(true).
Padding(0, 1)
headerMetaStyle = lipgloss.NewStyle().
Foreground(colorBrightBlack).
Padding(0, 1)
stripesStyle = lipgloss.NewStyle().
PaddingLeft(1).
Render(
lipgloss.NewStyle().Background(brandGray).Render(" ") +
lipgloss.NewStyle().Background(brandRed).Render(" ") +
lipgloss.NewStyle().Background(brandYellow).Render(" ") +
lipgloss.NewStyle().Background(brandBlack).Render(" "),
)
labelStyle = lipgloss.NewStyle().
Bold(true)
// Borders — foreground is set dynamically via borderFor()
baseBorderStyle = lipgloss.NewStyle().
BorderRight(true).
BorderTop(true).
BorderBottom(true).
BorderLeft(true).
BorderStyle(lipgloss.NormalBorder())
// Sidebar
procInactiveStyle = lipgloss.NewStyle().
PaddingLeft(1)
scrollArrowStyle = lipgloss.NewStyle().
PaddingLeft(1).
Foreground(colorBrightBlack).
Align(lipgloss.Center)
// Footer
footerStyle = lipgloss.NewStyle().
PaddingLeft(1)
// Scroll position indicator (floating top-right of output pane)
scrollIndicatorStyle = lipgloss.NewStyle().
Foreground(colorBlack).
Background(colorYellow).
Padding(0, 1)
// Copy mode
copyModeStyle = lipgloss.NewStyle().
Background(colorBlue).
Foreground(colorBlack)
// Search mode
searchMatchStyle = lipgloss.NewStyle().
Background(colorBrightBlack)
searchCurrentMatchStyle = lipgloss.NewStyle().
Background(colorYellow).
Foreground(colorBlack)
// Info mode
warnStyle = lipgloss.NewStyle().
Bold(true).
Foreground(colorYellow).
Background(colorBrightBlack)
hintStyle = lipgloss.NewStyle().
Foreground(colorBrightYellow)
// Group header in grouped sidebar mode
groupHeaderStyle = lipgloss.NewStyle().
PaddingLeft(1).
Bold(true).
Foreground(colorBrightBlack)
)
func statusIconChar(s process.Status) string {
switch s {
case process.StatusRunning:
return iconCharRunning
case process.StatusPending:
return iconCharPending
case process.StatusStopped:
return iconCharStopped
case process.StatusDone:
return iconCharDone
case process.StatusCrashed:
return iconCharCrashed
case process.StatusStandby:
return iconCharStandby
default:
return iconCharStopped
}
}
func statusIconColor(s process.Status) color.Color {
switch s {
case process.StatusRunning:
return colorGreen
case process.StatusPending:
return colorYellow
case process.StatusStopped, process.StatusDone:
return nil
case process.StatusCrashed:
return colorRed
case process.StatusStandby:
return colorBrightBlack
default:
return colorYellow
}
}
// Renders a single sidebar row with icon and name.
func subtleBg(isDark bool) color.Color {
if isDark {
return colorBrightBlack
}
return colorBrightWhite
}
// Selection fill color for highlighted rows.
func selectionBg(isDark bool) color.Color {
if isDark {
return selectionDark
}
return selectionLight
}
// borderFor returns the border style with a foreground appropriate for the
// current terminal background.
func borderFor(isDark, focused bool) lipgloss.Style {
s := baseBorderStyle.BorderForeground(subtleBg(isDark))
if focused {
s = s.BorderStyle(lipgloss.ThickBorder())
}
return s
}
type sidebarRow struct {
icon string
name string
iconColor color.Color
selected bool
unread bool
standby bool
innerW int
isDark bool
}
func renderSidebarRow(r sidebarRow) string {
nameW := max(r.innerW-2, 0) // 1 padding + 1 icon
selBg := selectionBg(r.isDark)
iconStyle := lipgloss.NewStyle().PaddingLeft(1).Foreground(r.iconColor)
if r.selected {
iconStyle = iconStyle.Background(selBg)
}
nameStyle := lipgloss.NewStyle().Width(nameW)
if r.selected {
nameStyle = nameStyle.Background(selBg)
} else if r.standby {
nameStyle = nameStyle.Foreground(colorBrightBlack)
} else if r.unread {
nameStyle = nameStyle.Bold(true)
}
return iconStyle.Render(r.icon) + nameStyle.Render(" "+r.name)
}
func helpStyles() help.Styles {
keyStyle := lipgloss.NewStyle()
descStyle := lipgloss.NewStyle().Foreground(colorBrightBlack)
sepStyle := lipgloss.NewStyle().Foreground(colorBrightBlack)
return help.Styles{
ShortKey: keyStyle,
ShortDesc: descStyle,
ShortSeparator: sepStyle,
Ellipsis: sepStyle,
FullKey: keyStyle,
FullDesc: descStyle,
FullSeparator: sepStyle,
}
}
func truncate(s string, maxLen int) string {
if maxLen <= 0 {
return ""
}
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen-1]) + "…"
}