Skip to content

Extract AdornLabel, AdornSelectChip, and shared Adorn CSS tokens#5032

Open
lukemelia wants to merge 6 commits into
cs-11280-adorn-hover-bar-overhangs-on-the-right-for-long-card-typefrom
extract-adorn-shared-primitives
Open

Extract AdornLabel, AdornSelectChip, and shared Adorn CSS tokens#5032
lukemelia wants to merge 6 commits into
cs-11280-adorn-hover-bar-overhangs-on-the-right-for-long-card-typefrom
extract-adorn-shared-primitives

Conversation

@lukemelia
Copy link
Copy Markdown
Contributor

Summary

Pull the shared visual spec out of the two Adorn UI call sites (operator-mode card overlays, search-results / card-chooser cards) so the look has a single source instead of being copy-pasted across files.

Stacks on top of #4999. Once this lands, #5009 + #5010 will be rebased on top and refactored to use the primitives.

What's extracted

  • <AdornLabel> — the teal flag-tab type label. Yields the default block so consumers populate it with .adorn-label-icon / .adorn-label-text / .adorn-label-dropdown children and own positioning. Background reads from --adorn-label-bg so consumers can shift to the darker accent when the underlying card is selected. Mirrors its clip-path vertically when the consumer sets data-side='bottom' (the operator-mode flip-below path).
  • <AdornSelectChip> — the 20×20 selection chip with the appropriate SVG icon based on @selected. The operator-mode caller wraps it in a button for interactivity; the card-chooser caller will mount it as a decorative element.
  • --adorn-accent-light + --adorn-accent color tokens, defined in the global app.css.
  • .adorn-stroke utility class — apply to any card-like element to get the standard 2px hover / 4px selected / darker-on-both outline. Reads both :hover and an explicit .hovered class so consumers that drive hover via JS (operator-mode overlays have pointer-events: none themselves) can opt in too.

Operator-mode refactor (this PR)

  • Markup: .adorn-label div + button + svg block → <AdornLabel> + <button><AdornSelectChip @selected={{isSelected}} /></button>.
  • CSS: drop the duplicated .adorn-label, .adorn-label-icon, .adorn-label-text, .adorn-label-dropdown, .adorn-select-button (visual), .adorn-select-icon, and stroke rules — they're now provided by the primitives. Keep operator-mode-specific bits: the selected→darker variable override (--adorn-label-bg: var(--adorn-accent)), menu trigger sizing, select-button positioning, and compact-mode container-query overrides (which use :global(...) to reach the primitive-owned class names).
  • The trackLabelOverflow modifier, boundary detection, menu, and the <span class='adorn-label-dropdown'> wrapper that keeps the dropdown a single flex item all stay in place — they're operator-mode-specific behavior, not visuals.

Followups

Test plan

🤖 Generated with Claude Code

lukemelia and others added 6 commits May 28, 2026 22:56
Two Adorn UI call sites (operator-mode card overlays in this branch
+ search-results / card-chooser cards in PR #5009 / #5010) had
copy-pasted the visual specification — the teal hover/selection
outline, the flag-tab type label, and the selection chip — into their
own component files. Pull the shared parts into reusable primitives
so the visual contract has a single source.

- New `<AdornLabel>` component renders the teal flag tab. Yields the
  default block; consumers populate it with `.adorn-label-icon` /
  `.adorn-label-text` / `.adorn-label-dropdown` children and own
  positioning. Background reads from `--adorn-label-bg` so consumers
  can shift to the darker accent when the underlying card is
  selected. Mirrors its clip-path vertically when the consumer sets
  `data-side="bottom"` (used by the operator-mode flip-below path).
- New `<AdornSelectChip>` component renders the 20×20 selection chip
  with the appropriate SVG icon based on the `@selected` arg. The
  operator-mode caller wraps it in a button for interactivity; the
  card-chooser caller will mount it as a decorative element.
- `--adorn-accent-light` / `--adorn-accent` color tokens and a new
  `.adorn-stroke` utility class live in the global `app.css`. Apply
  `.adorn-stroke` to any card-like element to get the standard 2px
  hover / 4px selected / darker-on-both outline; the utility reads
  both `:hover` and a `.hovered` class so consumers that drive hover
  via JS (e.g. operator-mode overlays, which have
  `pointer-events: none` themselves) can opt in too.

Refactor `operator-mode-overlays.gts` to use the primitives. The
trackLabelOverflow modifier, boundary detection, menu, and compact-
mode container-query overrides stay in place; the CSS shrinks to
just the operator-mode-specific bits (selected→darker variable
override, menu trigger sizing, select-button positioning).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…omponents

The AdornLabel and AdornSelectChip components used :global(...) inside
their scoped CSS so the inner content classes
(`.adorn-label-icon` / `.adorn-label-text` / `.adorn-label-dropdown`
inside the label, `.adorn-select-icon` inside the chip) would still
apply when consumers placed children with those class names.

Move the whole Adorn visual specification — flag clip-path / colors /
drop-shadow, the inner content-class rules, the chip CSS, and the
compact-mode @container overrides for the shared primitives — into
app.css instead. The components become pure markup wrappers with no
scoped CSS, and the operator-mode overlay's compact-mode @container
query keeps only its own operator-mode-specific overrides
(.adorn-label-menu sizing, .adorn-select-button positioning).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…app.css

The previous extraction relied on `:global(...)` rules inside the
AdornLabel / AdornSelectChip scoped CSS to reach yielded children,
and on global rules in app.css that any element with the right class
name picked up. Both break encapsulation.

Restructure so each Adorn primitive owns its own scoped CSS:

- New `<AdornContext>` component is the entry point. Wraps consumer
  markup in a `display: contents` div that declares the
  `--adorn-accent-light` / `--adorn-accent` tokens (cascade only;
  no globals), and exposes the hover/selection box-shadow utility
  via `:deep()` rules on any descendant with the `.adorn-stroke`
  class. The :deep is owned by AdornContext, not by consumers.
  AdornContext yields `{ Label, SelectChip }` — component references
  curried with `@compact` so the consumer doesn't have to re-pass
  it.
- `<AdornLabel>` now uses named blocks (`<:icon>`, `<:text>`,
  `<:dropdown>`) so every inner element (`.adorn-label-icon-slot`,
  `.adorn-label-text`, `.adorn-label-dropdown`) is rendered by the
  component, not by a consumer. Scoped CSS targets them directly,
  no :global needed. The icon slot cascades its size to its yielded
  contents via `> *`.
- `<AdornSelectChip>` is a simple span; its CSS is fully scoped.
- Both primitives accept a `@compact` arg and apply a `.compact`
  modifier class. No more @container queries inside the primitives.
- Operator-mode detects compact mode in JS (ResizeObserver on the
  actions-overlay overlay; checks the aspect-ratio > 2 / height <=
  57px threshold the previous @container query used) and passes the
  result to AdornContext as `@compact`. The same boolean toggles a
  `.compact` class on the overlay for the operator-mode-specific
  elements (menu trigger, select-button positioning) that aren't
  Adorn primitives.
- app.css no longer carries any Adorn rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AdornContext now wraps the outer container of adorn-decorated items
(operator-mode wraps its entire overlay each-loop in one AdornContext
rather than wrapping each card individually) and stands in as the
positioning boundary that trackLabelOverflow reads.

  - AdornContext stays layout-transparent (`display: contents`); it
    just publishes the Adorn tokens, the `:deep(.adorn-stroke)`
    outline utility, and a stable `.adorn-context` class for
    trackLabelOverflow to anchor against. The boundary lookup in
    findAdornLabelBoundary now finds the closest `.adorn-context`
    instead of `.stack-item-content`, which means any caller can
    decide where the label growth region begins by mounting the
    context.
  - AdornContext no longer yields curried `Label` / `SelectChip`.
    Consumers import `<AdornLabel>` / `<AdornSelectChip>` directly
    and pass `@compact` per item — operator-mode's compactness is
    per-card anyway, so the list-level curry was over-fitted.
  - Operator-mode-overlays wraps its `{{#each}}` in a single
    AdornContext and passes the per-card `@compact` directly to the
    primitive components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
trackLabelOverflow's `findAdornLabelBoundary` previously returned
the `<AdornContext>` element directly, but AdornContext renders as
`display: contents` (so it doesn't disrupt the consumer's layout)
which means its own `getBoundingClientRect()` is empty. Instead,
return the AdornContext's parent — the element the consumer mounted
the context inside of, which is the visible region whose rect we
actually want to bound the label against. Update AdornContext's doc
comment to describe the convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous trackCompact modifier instantiated one ResizeObserver
per overlay element. That's fine for typical hover cases (one
overlay) but scales linearly with multi-select, where every selected
card has its own overlay observer.

Move to one shared ResizeObserver per component instance. Each
trackCompact modifier registers its overlay (and the card element
it's an overlay for, via a WeakMap) with the shared observer on
mount and unregisters on teardown. The shared observer's callback
demultiplexes entries back to the card elements before updating
compactCards. Disconnect the observer on component willDestroy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lukemelia lukemelia force-pushed the extract-adorn-shared-primitives branch from c2a66bf to 5955387 Compare May 29, 2026 02:56
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