Skip to content

Add a shared BandedDiv component - #2073

Closed
GregorShear wants to merge 15 commits into
mainfrom
greg/banded-div
Closed

Add a shared BandedDiv component#2073
GregorShear wants to merge 15 commits into
mainfrom
greg/banded-div

Conversation

@GregorShear

@GregorShear GregorShear commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Changes

Adds theme.radius and src/components/shared/BandedDiv.tsx. Nothing consumes either yet; the service accounts UI (#1989) is the first caller and stacks on top of this PR.

theme.radius

A named border-radius scale on the augmented theme (sm 4px, md 8px, lg 12px, xl 16px, full), so roundness tracks an element's size and prominence and surfaces of the same class round the same way. Values are px strings, which sx and styled use literally — a number in borderRadius is multiplied by theme.shape.borderRadius instead.

BandedDiv

A rounded frame whose face sits over a colored band poking out one edge.

  • The band is pulled under the face by one corner radius, so the face's rounded corners curve away onto band color rather than opening gaps to the page. The face paints above it (zIndex: 1, opaque), and the frame's overflow: hidden clips the band's outer corners, so the band needs no radius of its own.
  • side picks the banded edge and defaults to left. bandColor is required, and accepts any color the browser can parse — named CSS colors included, normalized through a canvas fillStyle because MUI's color math rejects them.
  • label fills a slot spanning the band's visible run. A single element owns its own layout; a fragment, string, or array is laid onto a default centered row (caption type, small gap) so a bare icon-and-text label works without a wrapper. Long labels wrap and thicken the band.
  • Vertical bands rotate the label 270° as one unit and hand it the band's full run. Since a CSS transform never affects layout, RotatedLabel measures with a ResizeObserver: the label's natural height becomes the band's thickness, and the stretched spacer's length becomes the rotated label's width.
  • With onClick set, the frame becomes a ButtonBase and the band rests desaturated, coming up to full color on hover. desaturate and BAND_REST_SATURATION are exported so an owner can hold other surfaces to the same rest color, and BANDED_DIV_FACE_CLASS lets an owner restyle the face from the frame's hover.
  • The face defaults to an opaque padded surface with a border tinted toward the band color. faceSx merges onto the face; sx targets the frame.

Tests

Manually tested

  • Rendered through the service account cards on Service accounts #1989: bottom band on expiring keys, hover raising band and border together, band label wrapping at narrow card widths.

Automated tests

  • None. Typecheck, ESLint, and Prettier pass.

@GregorShear GregorShear changed the title greg/banded div Add a shared BandedDiv component Aug 28, 2026
@github-actions

Copy link
Copy Markdown

❌ Code Health

Introduces $\textcolor{red}{1}$ dead-code issue.

49 Unused files $\textcolor{red}{+1}$

File imported nowhere — delete (or import) it.

$\textcolor{red}{+}$ src/components/shared/BandedDiv.tsx
     src/hooks/useDelay.ts
     src/hooks/useDraft.ts
     src/pages/NoGrants.tsx
     src/pages/OAuth.tsx
     src/services/encryption.ts
     src/types/global.ts
     src/types/vitest.ts
     src/components/graphs/TaskHoursByMonthGraph.tsx
     src/components/tables/Link.tsx
…and 39 more

66 Unused exports

Exported symbol with no references outside its own file — un-export it, or delete it if unused entirely

     src/context/Theme.tsx : logoColors
     src/context/Theme.tsx : intensifiedOutlineThick
     src/context/Theme.tsx : tableAlternateRowsSx
     src/context/Theme.tsx : draggableChipIconSx
     src/context/Theme.tsx : hiddenButAccessibleInput
     src/context/Theme.tsx : primaryColoredBackground_hovered
     src/context/Theme.tsx : detailsPanelBgColor
     src/context/Theme.tsx : menuBackgroundColor
     src/context/Theme.tsx : flexGrowToSiblingsSx
     src/context/Theme.tsx : shardTableRow
…and 56 more

30 Unused exported types

Exported type with no references outside its own file — un-export it, or delete it if unused entirely

     src/utils/billing-utils.ts : FREE_GB_BY_TIER
     src/types/index.ts : InferredSchemas
     src/types/index.ts : Shard
     src/components/shared/WizardDialog/index.ts : WizardStep
     src/api/dataPlanes.ts : AwsDnsEntry
     src/stores/ShardDetail/types.ts : TaskShardDetailsWithShard
     src/stores/ShardDetail/types.ts : ShardDetails
     src/components/tables/Logs/types.ts : RefreshLogsFunction
     src/types/schemaModels.ts : CollectionSchema
     src/components/editor/Bindings/types.ts : SelectedCollectionChangeData
…and 20 more

14 Unused exported enum members

An enum member referenced nowhere

     src/services/supabase.ts : CONNECTOR_TAGS
     src/services/supabase.ts : DRAFTS_EXT
     src/services/supabase.ts : TASKS_BY_DAY
     src/stores/Tables/hooks.ts : accessGrants
     src/stores/Tables/hooks.ts : accessLinks
     src/stores/Tables/hooks.ts : billing
     src/stores/Tables/hooks.ts : connectors
     src/stores/Tables/hooks.ts : entitySelector
     src/stores/Tables/hooks.ts : prefixes
     src/stores/Tables/hooks.ts : prefixAlerts
…and 4 more

5 Unused dependencies

In package.json but never imported

     package.json : @mui/lab
     package.json : @testing-library/jest-dom
     package.json : @urql/exchange-retry
     package.json : logrocket-react
     package.json : stripe

3 Unused devDependencies

In package.json devDependencies but never used

     package.json : @types/logrocket-react
     package.json : @types/react-inspector
     package.json : sharp

`theme.radius` gives roundness a named scale that tracks an element's size and prominence, so surfaces of the same class round the same way. Values are px strings, which `sx` and `styled` use literally — a number in `borderRadius` is multiplied by `theme.shape.borderRadius` instead.
BandedDiv generalizes the tucked-band mechanism: a rounded frame whose face sits over a colored band poking out one edge, selected by a side prop. The band takes an optional label, read vertically when the band is on the left or right. With an onClick set the frame becomes a ButtonBase and the band rests at a desaturated color, coming up to full color on hover. The desaturate helper and rest-saturation constant come with it, exported for owners that hold other surfaces to the band's color scheme.
The no-band state moves from an omitted side to an omitted bandColor, so AccountCard now passes side=bottom unconditionally and gates the band on the expiry color.
A left or right band now rotates the whole label node 90 degrees with a transform, so icons turn with the text. A transform never affects layout - the rotated label would keep its wide horizontal box and stretch the band - so RotatedLabel measures the node and swaps its dimensions onto a spacer, centering the rotated content absolutely inside it.

The band no longer imposes a row Stack on the label; the node owns its internal layout. That also restores the icon-to-text gap the extraction dropped: the band's Stack spacing never applied inside a fragment label.
A BandedDiv without a band was the component's name telling a lie, so bandColor is now required and the band always renders. Also folds in the vertical label rotation direction change to 270 degrees, reading bottom-to-top.
MUI's decomposeColor rejects named CSS colors, so bandColor=tomato threw from getContrastText. The band color now round-trips through a canvas fillStyle, which has the browser normalize any color it can parse to hex or rgba() before the MUI color math. The context resets to black between calls, so an unparseable color degrades to a black band instead of silently reusing the previous call's color.
A bare BandedDiv now renders a padded theme-paper face instead of a transparent one, so the band tuck reads without the caller styling the face. faceSx merges over the default, so existing consumers are unchanged.
The face's default border now matches the service account cards exactly, using the same defaultOutline constant instead of the divider token.
The face's default border now carries the band color the way the service account cards do: desaturated at rest when the frame is interactive, rising to full color on hover together with the band; full color always when static. A label-less band also holds a minimum visible run past the overlap, so it reads as a band rather than collapsing to its padding.
The label node now renders into a slot stretched across the band's visible run whichever side the band is on, and owns its own alignment inside it. Horizontal bands stretch the label through a flex slot. Vertical bands rework RotatedLabel: the spacer stretches along the band, its measured length becomes the rotated node's width, and the label's natural height stays the band's thickness. A label that centers, spreads, or fills behaves identically on any side.
A fragment, string, or array label now renders on a centered row with a small gap between children, caption type, and no wrapping, so an icon-and-text label needs no wrapper Stack. A single element keeps the full-run canvas and owns its own layout and alignment. The default applies in both orientations, since the wrapped label feeds the same slot and RotatedLabel paths.
Merged onto the band over its defaults. The band's text styling (caption type, weight 600, nowrap) moves from the default label wrapper onto the band element itself, so it inherits into any label and a bandSx font size or weight override actually wins.
The only realistic band overrides are label text styles, and a single-element label already owns those. Everything else bandSx could reach is the tuck mechanism or the bandColor system, which callers should not mutate. Bare labels keep the caption/600 defaults inherited from the band.
Drops the nowrap default so a long label wraps and thickens the band; the vertical path already feeds the wrapped height back through its measurements. Wrapping makes the default row width-constrained, and an SVG's minimum size resolves to zero under flex shrink, so icons in the default row are pinned with flex none to keep them at their given size.
@GregorShear

Copy link
Copy Markdown
Contributor Author

Unwinding the split — BandedDiv goes back into greg/access (#1989).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant