# Scoped stylesheet

An additional consumer entry built alongside the default `dist/index.css`. Identical rules,
wrapped in `@scope ([data-mds-root])`, so MDS styles only apply inside containers the host
app opts in to — useful when MDS runs next to another stylesheet (Tailwind, a legacy global
sheet, another design system) in the same document.

- Output: `css/dist/mds-scoped.css`
- Import: `@innovaccer/design-system/css/scoped` or `…/css/dist/mds-scoped.css`
- Build: `npm run build-css` (gulp, builds both bundles) or `npm run build-css-scoped`

The default `dist/index.css` bundle is unchanged — this is purely additive.

## Usage

```html
<div data-mds-root>
  <!-- MDS components -->
</div>
```

Everything outside `[data-mds-root]` is untouched by MDS, and MDS components inside it are
unaffected by the host app's global styles that would otherwise reach them.

The scope root is yours to style — MDS declares nothing on it. If you put `data-mds-root` on a
custom element, remember that custom elements default to `display: inline`, so give it a
`display` of its own:

```css
ui-shell-app {
  display: block;
}
```

## How the build composes it

`scripts/build-scoped.mjs` reads the same sources, in the same order, as the default build
(both import the list from `scripts/css-sources.cjs`, so they cannot drift), then:

1. **Hoists global-namespace at-rules** — `@keyframes`, `@font-face`, `@property` and
   `@counter-style` above the `@scope` block. Their names are document-global and are not
   scopable; leaving them inside would silently drop them. The build throws if one is nested
   inside another at-rule, where the hoist cannot reach it.
2. **Remaps `:root` / `html` / `body` / `:host` to `:scope`** so design tokens attach to the
   scope root. Without this, every custom-property reference in the bundle resolves to nothing.
3. **Adds a root-anchored variant of every selector** — `.Backdrop` becomes
   `.Backdrop, :scope.Backdrop`. See *Overlays* below.
4. **Wraps the result** in `@scope ([data-mds-root]) { … }`.

Step 3 costs about +28% in raw bytes (~80 KB, considerably less gzipped).

Nothing is declared on the scope root itself. A `[data-mds-root]` element is consumer
markup, so any default MDS set there would compete with the consumer's own layout.

## Overlays

Modal, Sidesheet, FullscreenModal, Backdrop, Popover, Tooltip, Dropdown, Menu and the
Listbox drag ghost all render through `ReactDOM.createPortal` into `document.body` — outside
the consumer's `[data-mds-root]`. Without special handling they would receive neither
component rules nor tokens, and render completely unstyled.

Inside `@scope`, a rule like `.Backdrop` is implicitly `:scope .Backdrop`, so it matches
**descendants of a scope root only**. Those portaled elements therefore have to be scope
roots in their own right, and there is no MDS-owned ancestor to hang that on. So the build
adds them to the prelude directly, targeting markup the components *already* render:

```css
@scope (
  [data-mds-root],                              /* your opt-in container */
  body > .Overlay-wrapper:has([data-layer]),    /* Modal, Sidesheet, FullscreenModal */
  body > [data-test='DesignSystem-Popover'],    /* Popover, Tooltip, Dropdown, Menu, … */
  body > [data-floating-ui-portal],             /* same components, via @floating-ui/react's FloatingPortal */
  body > [data-test='DesignSystem-Backdrop'],   /* Backdrop */
  body > .Listbox-item--draggable               /* Listbox drag ghost */
)
```

`body > [data-floating-ui-portal]` is a second, additive root for the same Popover-built
components: `PopperWrapper` now renders through `@floating-ui/react`'s `FloatingPortal`, which
inserts its own `<div data-floating-ui-portal>` directly under `body` and nests the
`data-test='DesignSystem-Popover'` element one level inside *that* — no longer a direct child
of `body`. `data-floating-ui-portal` is set unconditionally by the library itself, so it keeps
matching regardless of a component's (now overridable via a `dataTest` prop) `data-test` value.

Every popper-based component shares one root — `Popover` renders the popup element itself and
Tooltip/Dropdown/Menu all go through it — so a single selector covers them all.

Each selector is qualified so it cannot collide with a host app's own class names:

- **`body >`** — all of these land as direct children of `<body>`, so a colliding host class
  would have to be at body level too.
- **`:has([data-layer])`** — `data-layer` is an MDS-only attribute present on all five portal
  roots. `.Overlay-wrapper` is a generic enough name that a host app could plausibly reuse it,
  so the class alone is not enough. It matches a *descendant* rather than a child because Modal
  wraps its container in `OutsideClick` when `backdropClose` is set.
- **`data-test='DesignSystem-*'`** — already namespaced.

The root-anchored selector variants from step 3 are what let those roots style *themselves*:
`.Backdrop` alone would only match descendants. Because *every* rule gets a variant, all rules
matching a given scope root are bumped by the same `:scope` (0,1,0), so relative specificity —
and therefore the cascade between MDS rules — is preserved.

`css/scripts/portal-roots.cjs` is the single source of truth for this list, shared with
`core/utils/__tests__/scopedPortalRoots.test.tsx`. That test renders each overlay and asserts
the selectors still match, so if a component ever stops rendering one of these hooks the build
fails loudly instead of silently shipping unstyled overlays.

### No component changes required

This is the reason the prelude keys off existing markup rather than a dedicated attribute:
nothing in `core/` changes, so **the scoped stylesheet can be updated independently of the
library version**. Consumers swap the CSS file without rebuilding or upgrading, and it works
against releases already in the wild.

## Custom scope selectors

`@scope`'s prelude is a forgiving selector list, so attribute and class patterns work:

```bash
MDS_SCOPE_SELECTOR='[data-mds-root], [class^="ui-"][class$="-app"]' npm run build-css-scoped
MDS_SCOPE_OUTPUT='ui-apps.css' MDS_SCOPE_SELECTOR='ui-shell-app, ui-admin-app' npm run build-css-scoped
```

One selector list in a single `@scope` block covers many hosts — you do not need a stylesheet
per app.

`MDS_SCOPE_SELECTOR` replaces only the *consumer* part of the prelude. The portal-root
selectors are always appended, so MDS's overlays stay scoped whatever you scope on. The build
logs the final prelude it used.

CSS has **no tag-name wildcard**, so a pattern like `ui-*-app` cannot be expressed as a
selector: `^=` / `$=` / `*=` match attribute *values*, and a tag name is not an attribute.
For custom elements following a naming convention, either enumerate the tags in
`MDS_SCOPE_SELECTOR`, or have the host stamp `data-mds-root` onto matching elements at
runtime (a `MutationObserver` plus `/^ui-.*-app$/.test(el.tagName.toLowerCase())`).

## Caveats

- **Browser support**: `@scope` requires Chrome/Edge 118+, Safari 17.4+, Firefox 128+. There
  is no fallback — an older browser drops the whole stylesheet as an unknown at-rule and
  renders MDS unstyled. Serve `dist/index.css` to browsers you still support.
- **`@keyframes` names stay global.** They are hoisted out of `@scope`, so animation names
  still collide with the host app's. `Spinner`'s were renamed to `mds-spin` / `mds-rotate`;
  generic ones such as `fadeIn`, `fadeOut` and `shimmer` remain exposed.
- **Portaled overlays get base tokens**, not per-root token overrides: a themed
  `[data-mds-root]` does not affect overlays, which are scope roots of their own under
  `<body>`. They are deliberately not re-parented into the app's scope root, because a
  `transform` / `filter` / `contain` on an ancestor there would become the containing block
  and break `position: fixed` overlays. Declare token overrides on `:root` so both the
  in-tree components and the body-level overlays pick them up.
- **The drag ghost has the weakest qualifier.** No `data-test` exists on it, so
  `body > .Listbox-item--draggable` is all that is available. Worth tightening with a
  `data-test` next time component changes ship.
