cssints 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/check.mjs CHANGED
@@ -1,6 +1,6 @@
1
+ import { i as shown, r as ratio, t as luminance } from "./color-BB8ipynT.mjs";
1
2
  import { s as ContrastError } from "./registry-8sYvlzyn.mjs";
2
3
  import { t as fallbackOf } from "./lowered-DOJEDRFM.mjs";
3
- import { i as shown, r as ratio, t as luminance } from "./color-BB8ipynT.mjs";
4
4
  import { Effect } from "effect";
5
5
  // src/check.ts
6
6
  /**
package/dist/clay.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { a as registry } from "./registry-8sYvlzyn.mjs";
2
- import { a as createTokens$1, i as createGlobalTheme$1, j as fail, s as sets } from "./theme-BbKT-rP-.mjs";
3
- import { n as valueOf, t as together } from "./contrast-BB0wimFI.mjs";
4
1
  import { n as parseColor } from "./color-BB8ipynT.mjs";
2
+ import { n as valueOf, t as together } from "./contrast-BB0wimFI.mjs";
3
+ import { a as createTokens$1, i as createGlobalTheme$1, j as fail, s as sets } from "./theme-BbKT-rP-.mjs";
4
+ import { a as registry } from "./registry-8sYvlzyn.mjs";
5
5
  import { Formatter, Predicate } from "effect";
6
6
  import { clampRgb, formatHex } from "culori/fn";
7
7
  // src/clay.ts
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { a as GProp, n as GFn } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  import { properties } from "@cssints/css-grammar/mdn";
5
5
  type B = {
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { a as GProp, n as GFn, t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  import { properties } from "@cssints/css-grammar/mdn";
5
5
  type B = {
@@ -1,6 +1,6 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, H as keyframes, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { a as GProp } from "../group-types-DrMdYewQ.mjs";
3
2
  import "../group-common-BUdf_ry9.mjs";
3
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, H as keyframes, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
4
4
  import { properties } from "@cssints/css-grammar/mdn";
5
5
  type B = {
6
6
  id: "css-animations-1";
@@ -1,6 +1,6 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, H as keyframes, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { a as GProp } from "../group-types-DrMdYewQ.mjs";
3
2
  import "../group-common-BUdf_ry9.mjs";
3
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, H as keyframes, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
4
4
  import { properties } from "@cssints/css-grammar/mdn";
5
5
  type B = {
6
6
  id: "css-animations-2";
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  type B = {
5
5
  id: "css-conditional-3";
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  type B = {
5
5
  id: "css-conditional-4";
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { a as GProp, t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  import { properties } from "@cssints/css-grammar/mdn";
5
5
  type B = {
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  type B = {
5
5
  id: "css-round-display-1";
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { a as GProp, t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  import { properties } from "@cssints/css-grammar/mdn";
5
5
  type B = {
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  type B = {
5
5
  id: "mediaqueries-4";
@@ -1,5 +1,5 @@
1
- import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
2
1
  import { t as GCondition } from "../group-types-DrMdYewQ.mjs";
2
+ import { A as ScvValue, B as global, C as CvValue, Dm as Fallback, E as PositionTry, F as cn, G as scv, K as style, M as Steps, N as Wrap, O as QueryWrap, Om as Part, P as attr, Q as ClassValue, R as cv, S as CvProps, V as group, Y as token, Z as within, _ as CSSStyleValue, a as DtcgUnit, c as Tokens, d as createTokens, f as layers, g as CSSImageValue, h as CSSColorValue, i as DtcgShadow, j as StateWrap, k as ScvConfig, km as Style, l as check, m as CSS, n as DtcgColor, o as DtcgValue, p as AttrOp, r as DtcgCubicBezier, s as Overrides, t as CheckOverrides, u as createGlobalTheme, v as CSSUnitValue, w as Keyframes, x as CvConfig, z as firstThatWorks } from "../theme-types-DRObPeMO.mjs";
3
3
  import "../group-common-BUdf_ry9.mjs";
4
4
  type B = {
5
5
  id: "mediaqueries-5";
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { a as registry, d as MacroError, f as MemberError, o as warn, r as forgetKernels, t as Registry } from "./registry-8sYvlzyn.mjs";
2
2
  import { A as camel, C as hashClass, D as settled, E as minWidth, F as probed, M as failIn, N as isNode, O as toDecls, P as node, S as edge, T as merged, _ as PSEUDOS, a as createTokens, b as checkVars, c as tokensOf, d as style, f as token, g as PROPS, h as LAYER, i as createGlobalTheme, j as fail, k as VAR, l as CSSTokenValue, m as FUNCTIONS, n as addTheme, o as isDone, p as CONTEXTS, r as check$1, t as DONE, u as isStyle, v as carried, w as isFallback, x as dropsOf, y as chainDecl } from "./theme-BbKT-rP-.mjs";
3
3
  import { D as relation, S as addRuntime, _ as addKernel, i as byOrder, n as addGlobal, r as addLayers, y as kernelWarnings } from "./sheet-rwl57DmD.mjs";
4
- import { n as loweredOf } from "./lowered-DOJEDRFM.mjs";
5
4
  import { i as SCV_CALL, l as groups$1, n as CX_CALL, t as CV_CALL } from "./instrument-QGVM1E1B.mjs";
5
+ import { n as loweredOf } from "./lowered-DOJEDRFM.mjs";
6
6
  import { checkDeclarationNow, checkQuery, checkSelector, checkValueNow, isDeclarationError } from "@cssints/css-grammar/check";
7
7
  import { lackingOf, messageOf } from "@cssints/css-grammar/support";
8
8
  import { CSS, CSSColorValue, CSSImageValue, CSSStyleValue, CSSStyleValue as CSSStyleValue$1, CSSUnitValue } from "@cssints/css-grammar/values";
package/dist/plugin.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { C as Style, S as Part, _ as fn, a as Entry, b as scope, c as Made, d as Scope, f as Table, g as dependsOn, h as arg, i as Css, l as Members, m as append, n as ArgsDef, o as Impl, p as Typed, r as Chain, s as KernelDef, u as Pending, v as inGroup, x as ship, y as lacking } from "./scope-DzQLlev_.mjs";
1
+ import { C as Style, S as Part, _ as fn, a as Entry, b as scope, c as Made, d as Scope, f as Table, g as dependsOn, h as arg, i as Css, l as Members, m as append, n as ArgsDef, o as Impl, p as Typed, r as Chain, s as KernelDef, u as Pending, v as inGroup, x as ship, y as lacking } from "./scope-phC9Hofc.mjs";
2
2
  import { a as Kernel, c as ThemeRule, i as GlobalRule, n as Conflict, o as Runtime, r as Decl, s as Ship } from "./author-types-BNEUwM5s.mjs";
3
3
  export { type ArgsDef, type Chain, type Conflict, type Css, type Decl, type Entry, type GlobalRule, type Impl, type Kernel, type KernelDef, type Made, type Members, type Part, type Pending, type Runtime, type Scope, type Ship, type Style, type Table, type ThemeRule, type Typed, append, arg, dependsOn, fn, inGroup, lacking, scope, ship };
@@ -0,0 +1,13 @@
1
+ import "node:module";
2
+ // \0rolldown/runtime.js
3
+ var __defProp = Object.defineProperty;
4
+ var __exportAll = (all, no_symbols) => {
5
+ let target = {};
6
+ for (var name in all) __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true
9
+ });
10
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
11
+ return target;
12
+ };
13
+ export { __exportAll as t };
@@ -1,6 +1,5 @@
1
1
  import { r as Decl, s as Ship, t as BcdKey } from "./author-types-BNEUwM5s.mjs";
2
2
  import { t as ClassValue } from "./runtime-Cv7hGrWf.mjs";
3
- import { createRequire } from "node:module";
4
3
  import { CSS, CSS as CSS$1, CSSColorValue, CSSColorValue as CSSColorValue$1, CSSImageValue, CSSImageValue as CSSImageValue$1, CSSStyleValue, CSSStyleValue as CSSStyleValue$1, CSSUnitValue, CSSUnitValue as CSSUnitValue$1 } from "@cssints/css-grammar/values";
5
4
  import { properties } from "@cssints/css-grammar/mdn";
6
5
  import { DTCG_TYPES, DtcgType, Grammar, StyleObject, Syntax, TOKEN_TYPES, Token, TokenFn, TokenName, TokenSetFn, TokenType, ValidSyntax, ValidToken } from "@cssints/css-grammar/token";
@@ -1,4 +1,4 @@
1
- import { t as AliasesOf } from "./scope-DzQLlev_.mjs";
1
+ import { t as AliasesOf } from "./scope-phC9Hofc.mjs";
2
2
  /**
3
3
  * `surface().color(t.color.canvas).lighten(0.06).ink()`: a background that follows from the surface around it, however
4
4
  * many plain wrappers sit between. `color(<color>)` is an absolute base (default: the parent surface); `lighten(step)`,
package/dist/tui.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { a as registry } from "./registry-8sYvlzyn.mjs";
2
- import { a as createTokens$1, i as createGlobalTheme$1, j as fail, s as sets } from "./theme-BbKT-rP-.mjs";
3
- import { g as evaluate, h as css } from "./sheet-rwl57DmD.mjs";
4
- import { n as valueOf, t as together } from "./contrast-BB0wimFI.mjs";
5
1
  import { n as parseColor } from "./color-BB8ipynT.mjs";
2
+ import { n as valueOf, t as together } from "./contrast-BB0wimFI.mjs";
3
+ import { g as evaluate, h as css } from "./sheet-rwl57DmD.mjs";
4
+ import { a as createTokens$1, i as createGlobalTheme$1, j as fail, s as sets } from "./theme-BbKT-rP-.mjs";
5
+ import { a as registry } from "./registry-8sYvlzyn.mjs";
6
6
  import { Formatter } from "effect";
7
7
  import { clampRgb, formatHex, formatHex8 } from "culori/fn";
8
8
  // src/tui.ts
package/dist/vite.mjs CHANGED
@@ -1,8 +1,8 @@
1
+ import { r as verifyChecks } from "./contrast-BB0wimFI.mjs";
1
2
  import { c as EvalError, d as MacroError, f as MemberError, i as forgetWarnings, l as HookError, n as forget, p as SiteMissingError, t as Registry } from "./registry-8sYvlzyn.mjs";
2
- import { C as fallbackEntries, T as runtimeNames, a as closeNames, b as DERIVED, c as layerEntries, f as themedEntries, l as layerStatements, m as usesDerived, n as addGlobal, o as globalEntries, r as addLayers, s as globalNames, t as LAYERS, u as renderSheet, v as kernelEntries, w as runtimeModule, x as RUNTIME_ID } from "./sheet-rwl57DmD.mjs";
3
3
  import { a as instrument, c as writeSite, o as runtimeImport, s as tagged } from "./instrument-QGVM1E1B.mjs";
4
- import { r as verifyChecks } from "./contrast-BB0wimFI.mjs";
5
4
  import { i as serverId, n as gitRoot, o as sessionOf, r as register } from "./devtools-link-DN91p7g5.mjs";
5
+ import { C as fallbackEntries, T as runtimeNames, a as closeNames, b as DERIVED, c as layerEntries, f as themedEntries, l as layerStatements, m as usesDerived, n as addGlobal, o as globalEntries, r as addLayers, s as globalNames, t as LAYERS, u as renderSheet, v as kernelEntries, w as runtimeModule, x as RUNTIME_ID } from "./sheet-rwl57DmD.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { targets as targets$1, useTargets } from "@cssints/css-grammar/support";
8
8
  import { Cause, Clock, Config, Data, Effect, Exit, Formatter, Option, Predicate, Result, Schema, Semaphore } from "effect";
@@ -672,6 +672,7 @@ function cssints({ inner = false, exclude, devtools = false, opencode = false, e
672
672
  const siteOf = /* @__PURE__ */ new Map();
673
673
  let buildMap = false;
674
674
  const callsOf = /* @__PURE__ */ new Map();
675
+ const sheetEdits = {};
675
676
  const sheet = Effect.fnUntraced(function* (ids = [...usedBy.keys()]) {
676
677
  return yield* renderSheet(ids.flatMap((id) => usedBy.get(id)?.split("\n") ?? []));
677
678
  });
@@ -912,6 +913,7 @@ function cssints({ inner = false, exclude, devtools = false, opencode = false, e
912
913
  yield* drop(file);
913
914
  }
914
915
  yield* Effect.sync(() => forgetWarnings([file]));
916
+ yield* Effect.sync(() => sheetEdits.last = file);
915
917
  const graph = env.moduleGraph;
916
918
  const affected = [.../* @__PURE__ */ new Set([...depsOf.get(file) ?? [], ...inflight])].flatMap((id) => graph.getModuleById(id) ?? []);
917
919
  yield* Effect.sync(() => affected.forEach((m) => {
@@ -959,6 +961,9 @@ function cssints({ inner = false, exclude, devtools = false, opencode = false, e
959
961
  server = s;
960
962
  if (inner) return;
961
963
  evaluator.server = evaluator.needed ? void 0 : s;
964
+ s.environments.client.hot.on("cssints:edits:get", (_, client) => {
965
+ if (sheetEdits.measured) client.send("cssints:edits", sheetEdits.measured);
966
+ });
962
967
  s.environments.client.hot.on("cssints:sheet", (v) => {
963
968
  if (!s.environments.client.moduleGraph.getModuleById(SHEET)?.transformResult?.code.includes(v)) Effect.runSync(reloadSheet());
964
969
  });
@@ -983,6 +988,15 @@ function cssints({ inner = false, exclude, devtools = false, opencode = false, e
983
988
  if (id === SHEET) {
984
989
  if (build) return LAYERS;
985
990
  const code = runSync(hook("vite.load", id, sheet()));
991
+ if (this.environment.name === "client" && server) {
992
+ const classes = new Set([...usedBy.values()].flatMap((v) => v.split("\n"))).size;
993
+ sheetEdits.measured = {
994
+ file: sheetEdits.last ? relative(root, sheetEdits.last) : "",
995
+ bytes: code.length,
996
+ classes
997
+ };
998
+ server.environments.client.hot.send("cssints:edits", sheetEdits.measured);
999
+ }
986
1000
  return {
987
1001
  code,
988
1002
  map: sheetMap(code, siteOf)
package/docs/API.md CHANGED
@@ -1230,6 +1230,7 @@ More plugins on `scope()`, each a package of its own with its rules and limits i
1230
1230
  | [`@cssints/layout`](../packages/layout/README.md) | `stack()`, `cluster()`, `sidebar()`, `switcher()`, `cover()`, `autoGrid()` | Every Layout primitives as chain members |
1231
1231
  | [`@cssints/fonts`](../packages/fonts/README.md) | `font(family, { provider, ... })` → `.family()`, `.trim()` | `@font-face` and a metric-matched fallback face; cap-height trimming by `text-box`, else capsize's margins |
1232
1232
  | [`@cssints/reset`](../packages/reset/README.md) | `reset({ media?, prose? })` | A modern reset in a top-level `@layer reset` declared before the engine's, below every style; `@cssints/reset/<baseline group>` keeps only the declarations of that group |
1233
+ | [`@cssints/data`](../packages/data/README.md) | `data()` | `max`, `sum` and `count` of runtime values, computed by layout and printed by a counter |
1233
1234
  | [`@cssints/tokens`](../packages/tokens/README.md) | `/open-props`, `/radix`, `/flexoki`, `/reasonable-colors`, `/madoromi` | DTCG data for `createTokens` |
1234
1235
  | [`@cssints/gradient`](../packages/gradient/README.md) | `gradient(kind, direction, from, to, easing?, stops?)` | Eased colour stops mixed in oklab at build time |
1235
1236
  | [`@cssints/gradient-2d`](../packages/gradient-2d/README.md) | `meshGradient(mesh)`, `freeformGradient(points)` | `mesh-gradient()` / `freeform-gradient()` where the browser has them, else a runtime's canvas (this package's algorithms; lines and shapes not yet) |
@@ -1242,6 +1243,7 @@ More plugins on `scope()`, each a package of its own with its rules and limits i
1242
1243
  | [`@cssints/masonry`](../packages/masonry/README.md) | `masonry().columns().gap()` | `display: grid-lanes`, else CSS `columns` (top to bottom per column) |
1243
1244
  | [`@cssints/placeholder`](../packages/placeholder/README.md) | `placeholder(path, { mode? })` | An `<image>` from a JPEG or PNG read at build time: a gradient of its colours laid out as the image, or a tiny thumbhash PNG |
1244
1245
  | [`@cssints/ramp`](../packages/ramp/README.md) | `ramp(base, { mode, steps \| ratios, background })`, `overrides(ramp)` | An OKLCH palette as DTCG data for `createTokens`; the contrast mode meets each ratio against its background |
1246
+ | [`@cssints/gamut`](../packages/gamut/README.md) | `fit(color, { target?, method? })`, `readable(bg, { ratio?, toward? })`, `tones(color, count)` | Gamut and contrast maths at build time: the nearest colour sRGB or Display P3 can show, a text colour that meets a WCAG 2 ratio, and a tonal scale as DTCG data for `createTokens` |
1245
1247
  | [`@cssints/rybitten`](../packages/rybitten/README.md) | `color(hue, { cube })`, `wheel(steps, { cube })`, `shades(hue, lightness)`, `overrides(group)` | RYBitten's red-yellow-blue colours (36 historical cubes) as DTCG data for `createTokens`: a hue, a wheel of hues, a hue at several lightnesses |
1246
1248
  | [`@cssints/skeleton`](../packages/skeleton/README.md) | `skeleton().radius().color().highlight()` | Loading placeholders whose shimmer is in phase across the page (one clock on `:root`), static under reduced motion |
1247
1249
  | [`@cssints/reactive`](../packages/reactive/README.md) | `state()`, `toggle()`, `set()`, `value()`, `checked()`, `persist()`, `equals()`, `either()`, `on()`, `emit()`, `bind()`, `when()`, `inside()` | State as custom properties, events as data on the element: writes only in events, conditions and DOM output in plain CSS, wired by a 1.4 kB runtime shipped to every target; `value()`/`checked()` take the field's value, calls of `on()`, `bind()` or `state()` in one `cn()` append, `persist(value, key)` keeps a state across a reload, and `bind()` takes a source, a comparison (`equals()`), a choice of two literals (`either()`) or a literal — the one expression the runtime evaluates, because an attribute on the element cannot be computed in CSS (2026-10-31, cssints-ac3o). `emit(name)` is the one hand-off to JavaScript: `on({ submit: emit("save") })` writes no state and dispatches a bubbling `reactive:save` from the element the native event fired on, with `{ event, owner }` in its detail, which `listen(target, name, handler)` from `@cssints/reactive/runtime` hears and whose disposer it returns (2026-10-31, cssints-4s8d) |
@@ -1281,7 +1283,7 @@ createMachineStore(machine, { settled?, intent? }): { state, optimistic, project
1281
1283
  - **Debounce** coalesces a burst by event type; `flush()` delivers the final intent, `cancel(type)` drops a superseded one. The same clock drives `after` and a transition's timestamp.
1282
1284
  - **The clock is injected** (`systemClock` is the default); the harness's fake clock moves only on `advance(ms)`, and the harness logs `{ at, event, from, to, effect, context, cancelled }` per delivery (and per effect it issued).
1283
1285
  - **The state is an external store**: `getState()`, `subscribe(listener)` and `subscribe(selector, listener)`, the last one firing only when the slice changes.
1284
- - **`./solid`**: `state` is a `createStore` of `{ ...context, phase }` written once per transition, mutating only the changed fields; `optimistic` is a derived `createOptimisticStore` whose intent, written inside a Solid `action`, reverts to the committed snapshot at settle (a superseded intent rolls back); `projection` is a `createProjection` over `state`, so a reader of a view's field does not re-run when another field changes. A projection reads the committed frame, not an optimistic override, so the visual phase is read off `optimistic` directly. One store write per transition is the atomicity the reactive runtime gets from its one flush (cssints-tm9j).
1286
+ - **`./solid`**: `state` is a `createStore` of `{ ...context, phase }` written once per transition, mutating only the changed fields; `optimistic` is a second store over the same fields that follows `state` and carries an intent's phase on top, dropped when a transition settles (a superseded intent rolls back); `projection` is a `createProjection` over `state`, so a reader of a view's field does not re-run when another field changes. A projection reads the committed frame, not an optimistic override, so the visual phase is read off `optimistic` directly. One store write per transition is the atomicity the reactive runtime gets from its one flush (cssints-tm9j).
1285
1287
 
1286
1288
  Limits: one state at a time (no parallel states, history states or nested machines), effects are data the host runs, and a numeric event `id` is reserved for an effect handle. There is no Quint spec for this runtime yet; the harness is the oracle.
1287
1289
 
@@ -1512,9 +1514,76 @@ createTable(options: {
1512
1514
 
1513
1515
  `rows` may be a getter (`() => rows()`): it is read at attribute-read time, so a table whose rows arrive later keeps its sort and its selection instead of being rebuilt, and a selection made before the rows landed applies once they do. Selection is over ids, so it survives the host's re-sort. `rowProps` carries `aria-selected` and a click that selects; `toggleRow` / `toggleAll` are the programmatic path, and the row's checkbox stops its click from reaching the row so the two controls never toggle the same row twice. `selectAllProps` and `rowCheckboxProps` are `undefined` unless `selection` is `"multiple"` (one control over one selection is meaningless), and the select-all is a `role="checkbox"` rather than a native `input` because the mixed state a screen reader must hear is `aria-checked="mixed"`, not the DOM-only `indeterminate` property. It toggles the rows the table renders: a windowed table toggles its window and keeps the rest of the selection. A table that renders all of its rows and columns carries no `aria-rowcount` / `aria-colcount` (the markup already says it); pass them, and every cell gets its `aria-colindex`, only when the host windows. `numeric` is a layout hint for the host's skin: HTML has no attribute for it.
1514
1516
 
1517
+ ### Headless: `createGrid` (`@cssints/headless`)
1518
+
1519
+ The APG grid as state and attribute objects: one tab stop, 2-D navigation, a cell/row/column selection rectangle, sortable column headers, edit mode and a windowed body. `specs/apg/grid.qnt` (navigation/edit) is its model, with `grid-selection.qnt` and `grid-sort.qnt` for the two values; `packages/headless/test/grid.browser.test.tsx` replays and walks them in Chromium.
1520
+
1521
+ **The body is the platform's.** Movement and the roving tab stop are the V2 `focusgroup="grid"` contract (`focusGrid` below, `specs/patterns/focusgrid.qnt`): `gridProps.ref` builds the runtime, the owner's `focusgroup` attribute is rendered from the same definition, and arrows on both axes, Home/End (the row's ends), Ctrl+Home/End (the grid's), the edges and the last-focused memory are not this primitive's code. `row` / `col` are readings of the DOM — the owner's `focusin` maps the focused element back to its coordinate — and the host mirrors programmatic moves (`focus`, `clickCell`, PageUp/PageDown, Shift+Arrow) into DOM focus, as the test fixture does. The library keeps edit mode (`Enter` / `F2` / `a` / `Escape`, `lastEdit`), selection, sort, PageUp/PageDown (a grid defines no page movement, so the runtime never sees those keys), the mount window and the `aria-*` indices.
1522
+
1523
+ **Wrapping is the platform's `flow`, not `wrap`.** `wrapCols` means "Right on the last column continues into the next row's first cell", which APG's layout-grid example writes `data-wrap-cols`; the runtime spells that `flow` (continue into the adjacent row, wrapping at the outer edge). `wrapRows` is the block axis the same way. A data grid is hard on both axes, so the definition is `{ rowEdge: wrapCols ? "flow" : "none", colEdge: wrapRows ? "flow" : "none", memory: true }`; `setWrapCols`, `setWrapRows` and `setGridKind` re-apply it through the runtime.
1524
+
1525
+ **The header row is still the library's and outside the group.** Sortable column headers keep their own one-dimensional roving, and the APG grid deliberately omits Up/Down between the header row and the body; `headerProps` marks each header `focusgroup="none"` so neither provider pulls it into the body's item collection. Tab then walks from the header stop into the body stop, as two stops of the sequential order.
1526
+
1527
+ **The `tabindex` recipe.** Each cell target carries a constant author `tabindex="0"` (the native engine navigates only items with `tabIndex > -1`, and a `<div role="gridcell">` needs an author value to be focusable at all) and the runtime writes the single stop over it. With `focusTarget: "widget"` the widgets are the item set, so cells author `-1` and widgets `0`. A host must not bind `tabindex` reactively after these props. `focusgroupstart` rides on the current coordinate; with `memory: true` the platform prefers its own memory once focus has been inside, so the marker is what the first Tab entry lands on. `aria-activedescendant` is still emitted for a widget target: it is an announcement, while focus really sits on the widget.
1528
+
1529
+ ```ts skip
1530
+ createGrid(options?: {
1531
+ rows?: number; // 4
1532
+ cols?: number; // 3
1533
+ gridKind?: "data" | "layout"; // "data"
1534
+ wrapCols?: boolean; // layout only: Right/Left flow across rows (data-wrap-cols)
1535
+ wrapRows?: boolean; // layout only: Down/Up flow across columns (data-wrap-rows)
1536
+ focusTarget?: "cell" | "widget"; // "cell"
1537
+ gridReadonly?: boolean;
1538
+ selectable?: boolean; // aria-selected + the selection keys
1539
+ sortEnabled?: boolean; // sortable columnheaders (aria-sort)
1540
+ value?: number | undefined; // controlled packed selection (packSelection); 0 = nothing
1541
+ defaultValue?: number;
1542
+ onValueChange?(value: number): void;
1543
+ sortValue?: number | undefined; // controlled packed sort (packSort); -1 = unsorted
1544
+ defaultSortValue?: number;
1545
+ onSortChange?(value: number): void;
1546
+ page?: number; // PageUp/PageDown step in rows, 2
1547
+ id?: string;
1548
+ rowWindow?: { start: number; count: number } | undefined; // the mounted row range
1549
+ provider?: "auto" | "polyfill"; // "polyfill" forces focusGrid's fallback
1550
+ }): {
1551
+ rows(): number; cols(): number;
1552
+ row(): number; col(): number; // readings of the DOM focus
1553
+ hasFocus(): boolean; mode(): "navigate" | "edit";
1554
+ gridKind(): "data" | "layout"; wrapCols(): boolean; wrapRows(): boolean;
1555
+ focusTarget(): "cell" | "widget"; gridReadonly(): boolean;
1556
+ lastEdit(): "none" | "committed" | "cancelled";
1557
+ renderedRows(): ReadonlyArray<number>;
1558
+ isSelected(row: number, col: number): boolean;
1559
+ selection(): number; sort(): number;
1560
+ sortColumn(): number; sortDirection(): "none" | "ascending" | "descending";
1561
+ focusTargetPart(): string | null; // the data-part the host mirror focuses
1562
+ currentCellId(): string; // the cell carrying focusgroupstart
1563
+ gridProps: { role: "grid"; "aria-rowcount"; "aria-colcount"; "aria-readonly"; "aria-activedescendant";
1564
+ focusgroup: string; ref(element: HTMLElement): void; onFocusOut(event: FocusEvent): void };
1565
+ rowProps(row: number): { role: "row"; id; "aria-rowindex" };
1566
+ cellProps(row: number, col: number): { role: "gridcell"; id; tabindex: number; focusgroupstart: "" | undefined;
1567
+ ref(element: HTMLElement): void; "aria-rowindex"; "aria-colindex"; "aria-readonly"; "aria-selected";
1568
+ onClick(): void };
1569
+ widgetProps(row: number, col: number): { tabindex: number; focusgroupstart: "" | undefined;
1570
+ ref(element: HTMLElement): void };
1571
+ headerProps(col: number): { role: "columnheader"; id; focusgroup: "none"; tabindex; "aria-sort";
1572
+ onClick(): void; onFocus(): void; onKeyDown(event: KeyboardEvent): void };
1573
+ focus(): void; blur(): void;
1574
+ clickCell(row: number, col: number): void;
1575
+ setGridKind(kind: "data" | "layout"): void;
1576
+ setWrapCols(flag: boolean): void; setWrapRows(flag: boolean): void;
1577
+ setFocusTarget(target: "cell" | "widget"): void;
1578
+ setGridReadonly(flag: boolean): void; setSelectable(flag: boolean): void; setSortEnabled(flag: boolean): void;
1579
+ };
1580
+ ```
1581
+
1582
+ The selection and the sort are requests: uncontrolled by default (`defaultValue` / `defaultSortValue`), and a controlled host owns them with `value` / `sortValue`; `packSelection` and `packSort` are the injective values. `page` is the spec's PAGE constant: PageDown moves a page of rows and clamps to the last row, PageUp the same upward. A `rowWindow` is only the mount window — cell/row indices stay absolute — and the runtime re-collects when it moves, so the stop stays on a mounted cell or is re-seated on the first one. Edit mode is the spec's: a read-only grid and the last column never enter it, leaving commits, Escape cancels.
1583
+
1515
1584
  ### Headless: `createTabs` (`@cssints/headless`)
1516
1585
 
1517
- The APG tablist as state and attribute objects: roving tabindex, wrapping arrows, Home/End, automatic or manual activation, disabled tabs, and a selection that is a request (`value` + `onValueChange`) or uncontrolled (`defaultSelected`). `packages/apg-specs/specs/apg/tabs.qnt` is its model; `packages/headless/test/tabs.browser.test.tsx` replays it in Chromium.
1586
+ The APG tablist as state and attribute objects: the platform's single tab stop, arrows, Home/End and wrap; automatic or manual activation, disabled tabs, and a selection that is a request (`value` + `onValueChange`) or uncontrolled (`defaultSelected`). `packages/apg-specs/specs/apg/tabs.qnt` is its model (it imports `shared/focusgroup.qnt`); `packages/headless/test/tabs.browser.test.tsx` replays it in Chromium, on both providers.
1518
1587
 
1519
1588
  Its accessibility requirements are catalogued, gap by gap, in `packages/headless/docs/requirements/tabs.md` (the model is `packages/headless/docs/requirements/README.md`).
1520
1589
 
@@ -1523,6 +1592,7 @@ Close and rename are an **extension** (bean `cssints-gdv2`, the docs playground'
1523
1592
  ```ts skip
1524
1593
  createTabs(options?: {
1525
1594
  ...; // the plain tabs: items, value, defaultSelected, orientation, dir, activation, disabled, focusablePanels, id, tabId, panelId, onValueChange
1595
+ provider?: "auto" | "polyfill"; // "polyfill" forces @microsoft/focusgroup-polyfill even where the engine has focusgroup
1526
1596
  closable?: boolean; // false: closeProps and the Delete key; the host removes from onClose
1527
1597
  renamable?: boolean; // false: a double click edits the label inline; the host renames from onRename
1528
1598
  label?(index: number): string | undefined; // the tab's name: the buttons' labels and the rename draft
@@ -1532,6 +1602,8 @@ createTabs(options?: {
1532
1602
  ...;
1533
1603
  editing(): number | null; // the tab being renamed inline
1534
1604
  focusAfterClose(index: number): number; // the tab to focus once the removal landed
1605
+ tablistProps: { role: "tablist"; "aria-orientation"?: "vertical"; focusgroup: string; dir?: "ltr" | "rtl"; ref };
1606
+ tabProps(index): { id; role: "tab"; "aria-selected"; "aria-controls"; "aria-disabled"; focusgroupstart; tabindex?: -1; ref; onClick; onMouseDown; onFocus; onKeyDown };
1535
1607
  closeProps(index): { "aria-label": string; tabindex: -1; onClick: () => void };
1536
1608
  renameProps(index): {
1537
1609
  onDblClick: () => void;
@@ -1544,12 +1616,15 @@ createTabs(options?: {
1544
1616
  };
1545
1617
  ```
1546
1618
 
1547
- - **Close.** `closeProps(index)` is the tab's close button: `aria-label="Close <label>"` and `tabindex=-1`, because a tablist's tab stops are its tabs; the keyboard path is Delete on the focused tab. Focus after a close goes to the tab that slid into the closed one's place, or to the previous tab when the last one closed: `focusAfterClose(index)` names that index in the shortened list (call it before the host removes the tab). Closing the last remaining tab is refused, as the spec refuses an empty tablist; a closed tab that was selected asks for the tab that takes the focus.
1548
- - **Rename.** A double click begins; the tab renders `inputProps` instead of its label. Enter commits, Escape reverts, blur commits, and a label that trims to nothing cancels. While editing, the input owns the keys: the tablist's roving does not move. `begin(index)` is the programmatic start, for a tab the host has just added (append it, then `focusTab(index)` and `begin(index)`).
1619
+ - **The platform's half.** `tablistProps` carries the `focusgroup` attribute (`tablist nomemory wrap`, `block` when vertical) and a `ref` that builds the provider, so arrows along the axis, Home / End, wrap and the single tab stop are the browser's (Chromium 150+) or `@microsoft/focusgroup-polyfill`'s. The definition is **no memory**: the group is entered on the selected tab (`focusgroupstart`, emitted by `tabProps`), not on wherever focus was last. `tabProps` therefore renders **no roving `tabindex`** — the provider owns it; the only `tabindex` a tab carries is a static `-1` on a disabled tab, which is how both providers are kept out of it. A host whose tab is not natively focusable (a `<div role="tab">` holding the rename input, say) gives it its own `tabindex="0"`; the platform still keeps a single stop among them. `provider: "polyfill"` skips the feature detect (for a test, or a page that needs the fallback); `focus` is a reading of the DOM, not a second cursor.
1620
+ - **The library's half.** Selection is the delta: automatic activation selects on the `focusin` a move causes, manual activation only on Enter / Space, and a click selects in both modes. A click on an already-focused tab selects itself (no `focusin` fires); a pointer press that does move focus is asked once, so a controlled host is not asked twice. Selection and focus are decoupled when a controlled host rejects a request — the tab stop then stays with the user's focus, which is why `tabs.qnt` keeps `tabStop` beside `selected`.
1621
+ - **Close.** `closeProps(index)` is the tab's close button: `aria-label="Close <label>"` and `tabindex=-1`, because a tablist's tab stops are its tabs; the keyboard path is Delete on the focused tab. It is not an item of the group either, so the platform neither moves focus to it nor acts on its keys. Focus after a close goes to the tab that slid into the closed one's place, or to the previous tab when the last one closed: `focusAfterClose(index)` names that index in the shortened list (call it before the host removes the tab). Closing the last remaining tab is refused, as the spec refuses an empty tablist; a closed tab that was selected asks for the tab that takes the focus.
1622
+ - **Rename.** A double click begins; the tab renders `inputProps` instead of its label. Enter commits, Escape reverts, blur commits, and a label that trims to nothing cancels. While editing, the input owns the keys: the tab's handler ignores them (and the platform already leaves a native `<input>`'s arrows alone). `begin(index)` is the programmatic start, for a tab the host has just added (append it, then `focusTab(index)` and `begin(index)`).
1549
1623
  - **Ids.** `tabId(index)` and `panelId(index)` replace the generated `${id}-tab-<index>` / `${id}-tabpanel-<index>`, and `aria-controls` / `aria-labelledby` follow them. Use them when the host's markup or a deep link already names a panel (`#logs`): the alternative is a second id on the same element, which no attribute can carry.
1550
- - **Direction.** `dir: "rtl"` swaps ArrowLeft / ArrowRight on a horizontal tablist (a vertical one keeps Up / Down); the host still sets `dir` on its element for the visual order. The model `tabs.qnt` is ltr only, so the ported `direction` scenarios stay skipped; `packages/headless/test/tabs-requirements.browser.test.tsx` holds the swap.
1551
- - **The host's list.** `items` is read from the option on every read, so the bounds, the roving tab stop and `firstEnabled` follow a host that shrinks its list. `label` is read lazily too, so a renamed tab renames its buttons with it.
1624
+ - **Direction.** `dir: "rtl"` renders as `dir` on `tablistProps`: the platform maps ArrowLeft / ArrowRight per the element's direction, so a horizontal rtl tablist puts the next tab to the left and a vertical one keeps Up / Down. The model `tabs.qnt` is ltr only, so the ported `direction` scenarios stay skipped; `packages/headless/test/tabs-requirements.browser.test.tsx` holds the swap.
1625
+ - **The host's list.** `items` is read from the option on every read, so the bounds, the entry point and `firstEnabled` follow a host that shrinks its list, and the primitive re-reconciles the provider on a selection or count change (the native engine reads `focusgroupstart` itself). `label` is read lazily too, so a renamed tab renames its buttons with it.
1552
1626
  - The spec models a `deletable` flavour (Delete, focus to the next tab, the last refused) but neither a rename nor the callbacks, so the browser test, not the model, holds the extension.
1627
+ - **Migration (0.x, `minor`).** `TabProps` no longer carries the roving `tabindex` (spread `tabProps(i)` and bind none; a non-button tab needs its own `tabindex="0"`), and `Tabs.keyDown` is gone. `TablistProps` gained `focusgroup`, `ref` and `dir`, and `TabProps` gained `focusgroupstart` and `ref`.
1553
1628
 
1554
1629
  ### Headless: `createHotkeys` (`@cssints/headless`)
1555
1630
 
@@ -1769,7 +1844,7 @@ createDatePicker(options: {
1769
1844
  };
1770
1845
  ```
1771
1846
 
1772
- Every date is an ISO `YYYY-MM-DD`, read as a UTC midnight; `month()` and `labels.month` use 0-11. `monthDays()` is **six weeks around the shown month**, so a row can start in the previous month and end in the next one: every cell is a real focusable date (a blank cannot hold the roving focus), and a cursor that leaves the month pages the grid to where it landed. `cellProps(iso)` marks the chosen day `aria-selected`, `today` `aria-current="date"`, a day outside `min`/`max` `aria-disabled`, and gives `tabindex=0` to the roving day only: the selected day, else today when it is in the shown month, else the first day the bounds allow. Keys on the focused day: ←/→ a day, ↑/↓ a week, Home/End the week's first/last day, PageUp/PageDown a month, Shift+PageUp/PageDown a year (the day clamps, 29 February included), Enter/Space selects and closes, Escape closes without changing the value, Tab leaves. The month arrows page the month and move the roving tab stop; the DOM focus stays on the arrow, so a host that wants the next key to land on a day moves it after the cells re-rendered (the kit's nav buttons do). `attach()` is the popover's light dismissal and focus return, plus focus into the roving day on open; `contentStyle()` is its placement — the same page-space `position: absolute` box `createPopover` gives, so a popup that escapes a positioned or clipping ancestor is placed by the initial containing block, not by the trigger's parent. The host renders the cells, the CSS and the trigger's visible text; a segmented text field is a host composition of `createField` + `createSpinbutton` over the same day numbers, not part of this API.
1847
+ Every date is an ISO `YYYY-MM-DD`, read as a UTC midnight; `month()` and `labels.month` use 0-11. `monthDays()` is **six weeks around the shown month**, so a row can start in the previous month and end in the next one: every cell is a real focusable date (a blank cannot hold the roving focus), and a cursor that leaves the month pages the grid to where it landed. `cellProps(iso)` marks the chosen day `aria-selected`, `today` `aria-current="date"`, a day outside `min`/`max` `aria-disabled`, gives `tabindex=0` to the roving day only and the grid's `focusgroupstart` to that same day: the selected day, else today when it is in the shown month, else the first day the bounds allow. `gridProps` is the grid's `role` / `aria-*` only — no `focusgroup` attribute and no `ref`: the picker owns its keyboard and its roving day, and the grid's `row` / `col` mirror the cursor so `cellProps` can name the current cell. `attach()` focuses that cell by id, not by `[tabindex="0"]` (which no longer names one element in a grid whose platform owns the stop). Keys on the focused day: ←/→ a day, ↑/↓ a week, Home/End the week's first/last day, PageUp/PageDown a month, Shift+PageUp/PageDown a year (the day clamps, 29 February included), Enter/Space selects and closes, Escape closes without changing the value, Tab leaves. The month arrows page the month and move the roving tab stop; the DOM focus stays on the arrow, so a host that wants the next key to land on a day moves it after the cells re-rendered (the kit's nav buttons do). `attach()` is the popover's light dismissal and focus return, plus focus into the roving day on open; `contentStyle()` is its placement — the same page-space `position: absolute` box `createPopover` gives, so a popup that escapes a positioned or clipping ancestor is placed by the initial containing block, not by the trigger's parent. The host renders the cells, the CSS and the trigger's visible text; a segmented text field is a host composition of `createField` + `createSpinbutton` over the same day numbers, not part of this API.
1773
1848
 
1774
1849
  ### Headless: `createPanZoom` (`@cssints/headless`)
1775
1850
 
@@ -1822,6 +1897,14 @@ The host owns the box and the look: the viewport's size and `overflow: hidden`,
1822
1897
 
1823
1898
  The APG menu family (`createMenu` for the menu button and the checkable menus, `createMenubar` for the roving bar) as state and attribute objects. Its behaviour is `specs/apg/menu-button.qnt`, `menu-checkable.qnt` and `menu-menubar.qnt`, and `packages/headless/test/menu-button.browser.test.tsx` and `packages/headless/test/menu.browser.test.tsx` replay the generated models, ported scenarios and seeded differential walks against it.
1824
1899
 
1900
+ The movement is the platform's `focusGroup` (`shared/focusgroup.qnt`, `./focusgroup`), one segment per level: the
1901
+ list is `focusgroup="menu"` (block axis, wrap), the bar is `focusgroup="menubar"` (the declared orientation and
1902
+ `wrap`), and each submenu is its own `menu` segment — the platform's contract is per segment, and a popup's list
1903
+ is a segment only while the popup is shown, so a closed one is suspended with `focusgroup="none"`. The arrows
1904
+ along each level's axis, Home / End, the wrap and the last-focused memory are the browser's (Chromium 150+) or
1905
+ `@microsoft/focusgroup-polyfill`'s, and **the platform owns the item `tabindex`**: the primitive never renders
1906
+ one. `provider: "polyfill"` forces the fallback (tests, hosts without the native attribute).
1907
+
1825
1908
  ```ts skip
1826
1909
  createMenu(options: {
1827
1910
  items: ReadonlyArray<string | { label: string; kind?: "item" | "checkbox" | "radio"; checked?: boolean; disabled?: boolean }>;
@@ -1839,6 +1922,7 @@ createMenu(options: {
1839
1922
  disabled?: ReadonlyArray<number>; // focusable, never activated
1840
1923
  id?: string | undefined;
1841
1924
  onItemClick?(index: number): void;
1925
+ provider?: "auto" | "polyfill"; // "auto": the native attribute where the engine has it
1842
1926
  }): {
1843
1927
  open(): boolean;
1844
1928
  focusLevel(): 0 | 1 | 2; // 0 outside, 1 on the trigger, 2 on an item
@@ -1848,12 +1932,44 @@ createMenu(options: {
1848
1932
  value(): number; // the packed view of the same state
1849
1933
  openMenu(): void; // a request, like every intent
1850
1934
  closeMenu(): void;
1851
- keyDown(key: string): void;
1935
+ keyDown(key: string): void; // the library's keys: Enter / Space, Escape, Tab
1852
1936
  // …items, triggerProps, menuProps, itemProps(index), type(), setFocusItem()
1853
1937
  };
1854
1938
  ```
1855
1939
 
1856
- **The checked state has one store and two views.** The readable pair (`checked` + `choice`) is the store; `value` is derived from it as `menu-checkable.qnt`'s `checked(wrap, choice)` (one checkbox bit * 5 + the radio). The pack has one checkbox bit, so only the readable pair can name a checkbox beyond item 0. A host may own either view; each has its request (`onCheckedValuesChange(checked, choice)` and `onCheckedValueChange(value)`), and where both views are given the readable pair wins. `defaultChecked` / `defaultChoice` start the pair and `defaultValue` the packed side.
1940
+ - **The list is the owner.** `menuProps` carries `role="menu"`, `hidden`, the definition as the `focusgroup`
1941
+ attribute (`"menu"` while shown, `"none"` while hidden) and the `ref` that builds the provider and registers
1942
+ the keydown listener **before** it — that order is what lets the primitive consume a key the platform would
1943
+ otherwise act on, and a delegated Solid `onKeyDown` would run at the document, after focus had moved. The row
1944
+ props carry no `tabindex` and no key handler: `onFocus` is how the primitive hears where the platform put the
1945
+ focus.
1946
+ - **A row must be focusable by nature.** The native engine writes no `tabindex` and only moves focus to an item
1947
+ that is tabbable already, so render a `<button>` or an `<a href>` per row (the polyfill decorates whatever the
1948
+ collection holds, and restores an authored `tabindex`). A `<div>` row works only on the polyfill.
1949
+ - **A test presses real keys.** The movement is the platform's, and a native engine ignores an untrusted
1950
+ `KeyboardEvent` dispatched from script: a test that presses an arrow declares it with `userEvent.keyboard`, or
1951
+ runs the widget on `provider: "polyfill"` (which is what `apps/ui/src/registry/menu/menu.browser.test.tsx`'s
1952
+ walks do, because their `tabindex` probe needs the rendered stop).
1953
+ - **It keeps the keys the platform does not infer**: Enter / Space activation, Escape, Tab (which closes and
1954
+ leaves), typeahead, and on a menubar the cross-axis keys — the open keys of the bar, `Right` into a submenu,
1955
+ `Left` back to the parent row or the previous bar item. The arrows along the axis, Home and End are not in
1956
+ `keyDown` and are never `preventDefault`ed by the primitive.
1957
+
1958
+ `createMenubar` is the same contract per level: `menubarProps` (`role="menubar"`, `aria-orientation`, the
1959
+ `focusgroup` attribute and the `ref`), `barItemProps(i)`, `menuProps(i)` and `submenuProps(i, j)` for the popups,
1960
+ `menuItemProps(i, j)` and `submenuItemProps(i, j, k)` for the rows. A vertical bar's own axis is the platform's
1961
+ too (`focusgroup="menubar block"`): `Up` / `Down` move between bar items and its cross-axis pair opens the menu,
1962
+ the same code path as `Right` / `Left` on a horizontal one. `focusLevel` (1 the bar, 2 a top-level menu, 3 a
1963
+ submenu) and `focusIndex` are what a host moves the DOM focus to; reading them is reading the DOM's own focus,
1964
+ written back by `onFocus`.
1965
+
1966
+ **Migration, if you rendered the item props before:** drop `tabindex` from your row markup and spread
1967
+ `menuProps` (or `menubarProps` and each `menuProps(i)` / `submenuProps(i, j)`) on the owner, so its `ref` builds
1968
+ the provider; render focusable rows. The row props gained `ref` (the provider's item collection), and
1969
+ `menuProps` / `menubarProps` gained `focusgroup` and `ref` and lost `onKeyDown`; `keyDown(key)` now carries only
1970
+ the library's keys.
1971
+
1972
+ **The checked state has one store and two views.** The readable pair (`checked` + `choice`) is the store; `value` is derived from it as `menu-checkable.qnt`'s `checked(wrap, choice)` (one checkbox bit \* 5 + the radio). The pack has one checkbox bit, so only the readable pair can name a checkbox beyond item 0. A host may own either view; each has its request (`onCheckedValuesChange(checked, choice)` and `onCheckedValueChange(value)`), and where both views are given the readable pair wins. `defaultChecked` / `defaultChoice` start the pair and `defaultValue` the packed side.
1857
1973
 
1858
1974
  **The open state is a request/commit pair**, like every other controlled primitive: `open` (+ `onOpenChange`) makes the host own it, `defaultOpen` starts it. Every intent that opens or closes — the trigger's click and its keys, Escape, an item's activation, Tab, an outside click in a kit, the programmatic `openMenu` / `closeMenu` — is a request, so a host that holds `open` keeps the menu open and the focus where it was; `menu-button.qnt` reverts the whole transition of a rejected request, focus included. A write the host makes on its own is not a request: it takes the focus to the first item when it opens and to the trigger when it closes, and a write of the value it already has moves nothing (no widget can observe it).
1859
1975
 
@@ -2483,13 +2599,19 @@ The APG Radio Group: one `radiogroup` with `role="radio"` items, where selection
2483
2599
  behaviour is `packages/apg-specs/specs/apg/radio.qnt`, replayed by
2484
2600
  `packages/headless/test/radio.browser.test.tsx` against the ported ariakit, base-ui and React Aria scenarios
2485
2601
  and a differential walk in six configurations (unchecked, checked, disabled, `activedescendant`, and a
2486
- controlled host that accepts or holds a request). `values` is the option list (a getter is fine, so a list
2487
- that arrives later is still one widget), `defaultValue` is the uncontrolled check and `value` +
2488
- `onValueChange` make the host own it.
2602
+ controlled host that accepts or holds a request). The same file proves the native path with real keys. `values`
2603
+ is the option list (a getter is fine, so a list that arrives later is still one widget), `defaultValue` is the
2604
+ uncontrolled check and `value` + `onValueChange` make the host own it.
2489
2605
 
2490
2606
  `value` may be a getter, and then `undefined` means "nothing checked" as a controlled state: it is the bare
2491
2607
  `undefined` prop that says uncontrolled.
2492
2608
 
2609
+ The roving flavor is the platform's `focusgroup="radiogroup"` contract (the browser's where it has one,
2610
+ `@microsoft/focusgroup-polyfill` otherwise, `provider: "polyfill"` forces the polyfill): both axes, Home / End,
2611
+ wrap and the item `tabindex` are its. The library keeps the deltas — selection follows focus, the entry point
2612
+ (`focusgroupstart`: the checked radio, else the first enabled one) and Space, which focusgroup does not
2613
+ provide.
2614
+
2493
2615
  ```ts skip
2494
2616
  createRadioGroup<T>(options: {
2495
2617
  values: ReadonlyArray<T> | (() => ReadonlyArray<T>); // one per radio
@@ -2498,21 +2620,21 @@ createRadioGroup<T>(options: {
2498
2620
  flavor?: "roving" | "activedescendant"; // "roving"
2499
2621
  disabled?: ReadonlyArray<number>; // indices never focused, checked or picked as the tab stop
2500
2622
  orientation?: "horizontal" | "vertical"; // sets aria-orientation only
2501
- dir?: "ltr" | "rtl"; // "ltr": under rtl the horizontal arrows swap
2623
+ dir?: "ltr" | "rtl"; // forwarded to the DOM: the platform reads it for the horizontal arrows
2502
2624
  id?: string; // base of `<id>-radio-<i>`
2503
2625
  describedby?: string | undefined; // aria-describedby on every radio
2626
+ provider?: "auto" | "polyfill"; // "auto"; "polyfill" forces the polyfill (a test, or a host that needs it)
2504
2627
  onValueChange?(value: T): void; // a request the host may ignore
2505
2628
  onFocus?(target: string): void; // "radio:<index>"
2506
2629
  onBlur?(target: string): void;
2507
2630
  }): {
2508
2631
  checked(): number | null; // the index, null when nothing is checked
2509
- focused(): number | null;
2632
+ focused(): number | null; // follows the DOM's focusin
2510
2633
  flavor(): RadioFlavor;
2511
2634
  values: ReadonlyArray<T>;
2512
2635
  value(): T | null;
2513
- focus(): void;
2636
+ focus(): void; // moves the focus *state*; the host moves the DOM focus
2514
2637
  blur(): void;
2515
- keyDown(key: string): void;
2516
2638
  click(index: number): void;
2517
2639
  setDisabled(indices: ReadonlyArray<number>): void;
2518
2640
  setFlavor(flavor: RadioFlavor): void;
@@ -2522,6 +2644,9 @@ createRadioGroup<T>(options: {
2522
2644
  "aria-orientation";
2523
2645
  "aria-activedescendant": string | undefined; // only the activedescendant flavor
2524
2646
  tabindex: 0 | undefined; // 0 only in the activedescendant flavor
2647
+ focusgroup: string | undefined; // the roving definition ("radiogroup wrap nomemory"), absent otherwise
2648
+ dir: "ltr" | "rtl" | undefined; // the option, forwarded
2649
+ ref: (element: HTMLElement) => void; // builds the platform's provider
2525
2650
  onKeyDown;
2526
2651
  onFocus;
2527
2652
  onBlur;
@@ -2533,21 +2658,37 @@ createRadioGroup<T>(options: {
2533
2658
  "aria-disabled": "true" | "false";
2534
2659
  "aria-describedby";
2535
2660
  type: "button"; // keeps a surrounding form from submitting
2536
- tabindex: 0 | -1 | undefined; // roving only: 0 for the tab stop
2661
+ focusgroupstart: "" | undefined; // roving: the platform's entry marker, on the checked radio
2662
+ focusgroup: "none" | undefined; // roving: opts a disabled radio out of the group
2663
+ tabindex: 0 | -1 | undefined; // the author value the platform needs; it owns the roving stop
2664
+ ref: (element: HTMLElement) => void;
2537
2665
  onClick;
2538
2666
  onFocus;
2539
2667
  };
2540
2668
  };
2541
2669
  ```
2542
2670
 
2543
- - **Two flavors, one machine.** `"roving"` (the default) keeps the tab stop on the radios: `tabindex=0` on
2544
- the checked radio, or on the first enabled one while nothing is checked, `-1` on the rest.
2545
- `"activedescendant"` puts the tab stop on the group (`tabindex=0` there, nothing on the radios) and names
2546
- the focused radio in `aria-activedescendant`; the host keeps the DOM focus on the group. The primitive
2547
- moves no DOM focus in either flavor: `focused()` reads synchronously, so the host applies it.
2548
- - **Selection follows the focus.** An arrow moves to the next enabled radio (wrapping) and requests it, Space
2549
- requests the focused one, a click focuses and requests, and Tab enters on the tab stop. `setPreselect`
2671
+ - **Two flavors, one machine.** `"roving"` (the default) is the platform's tab stop: an arrow, Home or End
2672
+ moves DOM focus, the primitive never renders a roving `tabindex` (the polyfill writes `0` / `-1`; the native
2673
+ engine writes nothing and keeps the stop in its focus controller), and a focus that arrives from outside
2674
+ only moves the state. `"activedescendant"` puts the tab stop on the group (`tabindex=0` there, nothing on
2675
+ the radios) and names the focused radio in `aria-activedescendant`: focusgroup models DOM focus moving
2676
+ between items, so this flavor keeps its keyboard in the primitive.
2677
+ - **Selection follows the focus.** An arrow moves to the next enabled radio (wrapping) _and requests_ it —
2678
+ the `focusin` the platform causes is the library's, since focusgroup leaves selection to the pattern. Space
2679
+ requests the focused one, a click focuses and requests, and Tab enters on the checked radio. `setPreselect`
2550
2680
  starts the check without a request, and `setDisabled` / `setFlavor` reset the focus.
2681
+ - **The tab stop is the platform's, and the entry point is the library's.** While focus is inside the group
2682
+ the stop is the focused radio — the native engine keeps it in its focus controller, the polyfill renders
2683
+ `tabindex=0` on it — and the primitive does not re-seat it, so a controlled host that rejects a request
2684
+ leaves the stop on the radio the user moved to. The definition is `nomemory` with `focusgroupstart` on the
2685
+ checked radio (else the first enabled one), so the next Tab entry — and the stop once focus leaves — is the
2686
+ checked radio: the rule users see, `radio.qnt` models the same way via `fgTabIndex` / `fgEntry` /
2687
+ `fgStopOnLeave`.
2688
+ - **A disabled radio is out of the group** (`focusgroup="none"`, `tabindex="-1"`): arrows and the tab stop
2689
+ skip it and Tab never lands on it. Against the native engine every other radio carries `tabindex="0"`,
2690
+ because that engine navigates only items with `tabIndex > -1` and a `<div role="radio">` is otherwise not
2691
+ focusable at all; the engine still gives the group one tab stop.
2551
2692
  - **Config remounts fire no request.** `setDisabled`, `setFlavor` and `setPreselect` write the uncontrolled
2552
2693
  local directly; with `value` set, what is checked is the prop's, so they only move the focus.
2553
2694
  - **Two divergences:** React Aria renders native `<input type="radio">` elements and carries the HTML
@@ -2556,8 +2697,8 @@ createRadioGroup<T>(options: {
2556
2697
 
2557
2698
  ArrowDown / ArrowRight are forward and ArrowUp / ArrowLeft backward, with `dir: "rtl"` swapping the
2558
2699
  horizontal pair; both axes work whatever `orientation` says, since that option only sets `aria-orientation`.
2559
- The arrows wrap past the ends and skip every disabled index; Home / End are not handled, as the pattern has
2560
- no such key.
2700
+ The arrows wrap past the ends and skip every disabled index; Home / End come from the platform, so the roving
2701
+ flavor handles them too (the spec has no such key).
2561
2702
 
2562
2703
  ### Headless: `createTooltipGroup` (`@cssints/headless`)
2563
2704
 
@@ -2644,21 +2785,135 @@ Escape on a trigger hides the tooltip (`escape()`) without moving the focus or t
2644
2785
  reopens it until that trigger is left and entered again — WCAG's dismissable part. No other key is bound:
2645
2786
  focus and hover are the other two ways in, and the pointer handlers above are the hoverable part.
2646
2787
 
2788
+ ### Headless: `focusGroup` (`@cssints/headless`)
2789
+
2790
+ The platform layer of the `focusgroup` contract — one tab stop, arrows along the axis, Home / End, wrap and
2791
+ the last-focused memory — the behaviour `packages/apg-specs/shared/focusgroup.qnt` models once for every
2792
+ widget. Two providers behind one definition:
2793
+
2794
+ ```ts skip
2795
+ focusGroup(owner: HTMLElement, options: {
2796
+ definition: {
2797
+ behavior: "toolbar" | "tablist" | "radiogroup" | "listbox" | "menu" | "menubar" | "none";
2798
+ wrap?: boolean; // false emits `nowrap`
2799
+ axis?: "inline" | "block"; // "block" for a vertical widget
2800
+ memory?: boolean; // false emits `nomemory`
2801
+ };
2802
+ items: () => ReadonlyArray<HTMLElement>; // read now and on every update()
2803
+ }): {
2804
+ attribute: string; // the definition as the focusgroup attribute
2805
+ update(definition?: definition): void;
2806
+ destroy(): void; // idempotent
2807
+ };
2808
+
2809
+ focusGroupPolyfill(owner, options); // the same, skipping the feature detect
2810
+ focusGroupAttribute(definition); // "toolbar block nowrap"
2811
+ supportsFocusGroup(); // 'focusGroup' in HTMLElement.prototype
2812
+ ```
2813
+
2814
+ - **Native first.** Where `'focusGroup' in HTMLElement.prototype` (Chromium 150+), the adapter sets the
2815
+ owner's `focusgroup` attribute and stops there: the browser owns the group, and it writes **no** `tabindex`
2816
+ — the tab stop lives in the focus controller, not in the markup.
2817
+ - **The polyfill otherwise.** The low-level `FocusGroup` class of `@microsoft/focusgroup-polyfill` (a
2818
+ dependency, MIT, 5.5 KB gz) over an array-backed item collection — never `polyfill()` and never a
2819
+ `MutationObserver`, so the host's own reconciliation drives `update()`. This provider renders the tab stop as
2820
+ `tabindex="0"` / `"-1"`, saving and restoring the author's values.
2821
+ - **Call `update()`** when the item set, an item's visibility or the definition changes. The adapter observes
2822
+ nothing.
2823
+ - **Teardown, in this order:** `update({ definition: {} })`, which restores the authors' `tabindex`, then
2824
+ `disconnect()`. `disconnect()` alone leaves `tabindex` rewritten (measured in Chromium 153 and 156).
2825
+ - **A consumed key** is a `preventDefault()` on `keydown`: both providers honour it. Register the listener
2826
+ that decides _before_ the adapter's own (the polyfill's is on the owner, so a Solid `onKeyDown`, delegated to
2827
+ the document, is too late — attach it in the owner's `ref`, as `createToolbar` does).
2828
+ - **`focusGroupPolyfill`** is for a test that must prove the fallback in an engine that has the native feature,
2829
+ and for a host that knows it needs the polyfill. It removes the owner's `focusgroup` attribute so the native
2830
+ engine stays out of the group, and hides the `focusGroup` IDL property for the one constructor call the
2831
+ polyfill's own feature detect guards.
2832
+
2833
+ ### Headless: `focusGrid` (`@cssints/headless`)
2834
+
2835
+ The platform layer of `focusgroup="grid"` (V2): two-dimensional directional navigation over a rectangular, spanless
2836
+ body, one tab stop, entry / memory — the behaviour `packages/apg-specs/specs/patterns/focusgrid.qnt` models. It is
2837
+ **ours** because V2 is unpublished: no engine ships `grid`, and `@microsoft/focusgroup-polyfill` 1.6.0 (npm's latest)
2838
+ has no grid token at all, while its `main` branch (merged 2026-09-29) has no built `dist`. One definition renders the
2839
+ attribute the platform will read and drives our movement.
2840
+
2841
+ ```ts skip
2842
+ focusGrid(owner: HTMLElement, options: {
2843
+ definition: {
2844
+ manual?: boolean; // "grid manual": the rows are focusgrouprow children, not a native table
2845
+ rowEdge?: "none" | "wrap" | "flow"; // the INLINE edges: rowwrap / rowflow
2846
+ colEdge?: "none" | "wrap" | "flow"; // the BLOCK edges: colwrap / colflow
2847
+ memory?: boolean; // false emits `nomemory`
2848
+ };
2849
+ items: () => ReadonlyArray<{
2850
+ element: HTMLElement; // the cell target of the coordinate
2851
+ row: number;
2852
+ col: number;
2853
+ cell?: HTMLElement; // when `element` is a descendant of its cell
2854
+ }>;
2855
+ provider?: "auto" | "polyfill"; // "polyfill" forces this runtime and writes no focusgroup attribute
2856
+ }): {
2857
+ attribute: string; // the definition as the focusgroup attribute
2858
+ update(definition?: definition): void;
2859
+ destroy(): void; // idempotent
2860
+ };
2861
+
2862
+ focusgridAttribute(definition); // "grid manual rowflow colwrap nomemory"
2863
+ supportsFocusGrid(manual?); // focusGroup.supports("grid") (and "manual")
2864
+ ```
2865
+
2866
+ - **What it owns.** The `focusgroup` attribute, exactly one `tabindex="0"` cell (the authors' values are saved and
2867
+ restored by `destroy()`), arrows on both axes, `Home` / `End` for the row's ends, `Ctrl+Home` / `Ctrl+End` for the
2868
+ grid's, the per-axis wrap / flow edges, `focusgroupstart` as the entry target, the last-focused memory and
2869
+ `nomemory`, and the `focusgrouprow` markers of a `manual` topology. A hard edge does not move focus and does not
2870
+ cancel the key.
2871
+ - **The edges are resolved, not tokenised.** `rowEdge` / `colEdge` are the per-axis result, so the attribute's broad
2872
+ `wrap` / `flow` (both axes) and its conflicts (`wrap flow`, `nowrap` with either) are the caller's parse; the
2873
+ attribute we emit is canonical — `rowwrap`, never `wrap` — and a hard axis emits nothing. A grid's default is hard
2874
+ (the explainer: wrapping "can disorient users in data grids"), which is the opposite of the V1 behaviours.
2875
+ - **`manual` is topology only.** It renders `grid manual` and marks each row with `focusgrouprow`; it changes no
2876
+ coordinate, no key and no tab stop. The host owns row discovery either way: `items` gives the coordinates, and a
2877
+ body that is not a rectangle (a ragged row, a span, a duplicate coordinate) decorates nothing and moves nothing —
2878
+ the explainer's "invalid grids do not run".
2879
+ - **A native control keeps its keys.** A cell target that is an `<input>` (not a checkbox or radio), `<textarea>`,
2880
+ `<select>`, `contenteditable`, `<iframe>` / `<object>`, or a `<video>` / `<audio>` with controls does not move the
2881
+ grid: its own behaviour claims the arrow, Home and End.
2882
+ - **Call `update()`** when the body, a `focusgroupstart` marker or the definition changes. Like `focusGroup`, the
2883
+ adapter observes nothing; a host that must consume a key first registers its own listener **before** calling
2884
+ `focusGrid` (a delegated Solid `onKeyDown` runs at the document, after this one).
2885
+ - **Native first, the same definition.** Where `focusGroup.supports("grid")` (and `"manual"`, for a manual topology)
2886
+ the runtime writes only the attribute and its row markers and the browser owns the movement and the tab stop — so
2887
+ the host authors `tabindex` on each cell target, as the explainer's markup does. The polyfill's compatibility trap
2888
+ (it polyfills `grid` even where a browser ships it) does not apply: this is not the polyfill.
2889
+ `provider: "polyfill"` skips the feature detect, drives this runtime and writes no `focusgroup` attribute, so the
2890
+ two engines cannot both act (a test, or a host that knows it needs the fallback).
2891
+ - **What it does not do** (the widget's, `specs/apg/grid.qnt` and `createGrid`): edit mode, selection, sort,
2892
+ activation, `PageUp` / `PageDown`, spans, ragged rows, virtualisation, `itemcontrols` (default for a grid,
2893
+ unsupported upstream), `aria-rowindex` / `aria-colindex` / role inference, and Tab / Shift+Tab (HTML sequential
2894
+ navigation, observed here only as focusout).
2895
+
2647
2896
  ### Headless: `createToolbar` (`@cssints/headless`)
2648
2897
 
2649
- The APG Toolbar: a row or column of controls behind one tab stop, with the roving tabindex of
2650
- `shared/focusgroup.qnt` and the delta of `packages/apg-specs/specs/apg/toolbar.qnt` (`aria-orientation`, the
2651
- author's wrap choice, disabled-but-focusable items, embedded controls that consume keys, and activation).
2898
+ The APG Toolbar: a row or column of controls behind one tab stop, with the platform's `focusGroup` and the
2899
+ delta of `packages/apg-specs/specs/apg/toolbar.qnt` (`aria-orientation`, the author's wrap choice,
2900
+ disabled-but-focusable items, embedded controls that consume keys, Tab out of a text field, and activation).
2652
2901
  `packages/headless/test/toolbar.browser.test.tsx` replays the ported ariakit, base-ui and React Aria
2653
2902
  scenarios and walks the model horizontally (no wrap) and vertically (wrapping); the fixture composes
2654
- `createMenu` for the spec's menu-button, which the primitive never imports.
2903
+ `createMenu` for the spec's menu-button, which the primitive never imports. The walk runs on the polyfill
2904
+ provider: native focusgroup ignores an untrusted `keydown` and writes no `tabindex`, and both are what the
2905
+ walk's programmatic steps and `tabindex` probe need (the adapter test covers the native path).
2655
2906
 
2656
- Platform: the roving tabindex is the `focusgroup="toolbar"` contract, so ship the
2657
- `@microsoft/focusgroup-polyfill` first and switch to the native attribute when Chrome/Edge is the floor.
2907
+ **Migration, if you rendered the item props before:** drop `tabindex` from your item markup and let the
2908
+ provider write it. `ToolbarItemProps` no longer carries `tabindex`, and the owner must now take
2909
+ `toolbarProps` (which carries `focusgroup`, `role`, `aria-orientation` and the `ref` that builds the
2910
+ provider). Nothing else about the item props changes; `toolbarProps` gained `focusgroup` and `ref` and no
2911
+ longer carries `onKeyDown`, because the `ref` registers that listener before the provider's.
2658
2912
 
2659
2913
  `items` is fixed at construction, as label strings or `{ label, kind, disabled }`, and `kinds` names a kind per
2660
2914
  index for the string form (`"button"` by default). `defaultFocus` is the index that starts focused, and
2661
- `focus: -1` means "outside the toolbar".
2915
+ `focus: -1` means "outside the toolbar". `provider: "polyfill"` forces the fallback (tests, hosts without the
2916
+ native attribute).
2662
2917
 
2663
2918
  ```ts skip
2664
2919
  createToolbar(options: {
@@ -2670,6 +2925,7 @@ createToolbar(options: {
2670
2925
  disabled?: ReadonlyArray<number>; // items disabled at mount, still focusable
2671
2926
  id?: string; // base of `<id>-item-<i>`
2672
2927
  onActivate?(index: number): void; // a button item, by click, Enter or Space
2928
+ provider?: "auto" | "polyfill"; // "auto": the native attribute where the engine has it
2673
2929
  }): {
2674
2930
  focus(): number; // -1 when the toolbar is left
2675
2931
  tabStop(): number;
@@ -2679,35 +2935,40 @@ createToolbar(options: {
2679
2935
  focusIn(): void; // Tab into the toolbar: the tab stop
2680
2936
  leave(): void;
2681
2937
  focusItem(index: number): void;
2682
- keyDown(key: string): void;
2683
2938
  tab(back: boolean): void;
2684
2939
  clickItem(index: number): void;
2685
2940
  activate(): void; // a button item
2686
2941
  toggleDisabled(index: number): void;
2687
- setOrientation(orientation: "horizontal" | "vertical"): void;
2688
- setWrap(wrap: boolean): void;
2942
+ setOrientation(orientation: "horizontal" | "vertical"): void; // update()s the provider
2943
+ setWrap(wrap: boolean): void; // update()s the provider
2689
2944
  toolbarProps: {
2690
2945
  role: "toolbar";
2691
2946
  "aria-orientation": "horizontal" | "vertical";
2692
- onKeyDown;
2947
+ focusgroup: string; // "toolbar", "toolbar block", ... — the definition as the attribute
2948
+ ref: (element: HTMLElement) => void; // builds the provider, registers the key handler first
2693
2949
  };
2694
2950
  itemProps(index: number): {
2695
2951
  id;
2696
2952
  role: "button";
2697
2953
  "aria-disabled": "true" | undefined;
2698
- tabindex: 0 | -1; // the roving tab stop
2954
+ ref: (element: HTMLElement) => void; // hands the item to the provider
2699
2955
  onClick;
2700
- onFocus;
2956
+ onFocus; // records the index: `focus` / `tabStop` follow the DOM focus
2701
2957
  };
2702
2958
  };
2703
2959
  ```
2704
2960
 
2961
+ - **The provider owns the tab stop.** There is no `tabindex` in `itemProps`, and there is no `keyDown`
2962
+ method: the arrows, Home / End and the wrap are the platform's, and `focus` / `tabStop` follow the DOM
2963
+ `focusin`. A host that needs to move focus calls `focusItem(index)` and focuses the element the props name.
2705
2964
  - **Disabled but focusable.** A disabled item keeps its place in the arrows and the tab-stop order and
2706
2965
  announces `aria-disabled="true"`; it is never activated, so `onActivate` does not fire for it.
2707
- `toggleDisabled(index)` flips the mask at runtime.
2966
+ `toggleDisabled(index)` flips the mask at runtime. It is `aria-disabled`, not the native attribute, because
2967
+ the platform skips a natively disabled item.
2708
2968
  - **Embedded controls consume their keys.** A `"text"` item swallows the axis navigation keys and a
2709
- `"spin"` item swallows Up / Down and Home / End, so typing or stepping inside an item never moves the
2710
- toolbar's focus. The embedded control's own value is not modelled: the host renders it and owns it.
2969
+ `"spin"` item swallows Up / Down and Home / End (`preventDefault` on the owner's keydown, before the
2970
+ provider's listener), so typing or stepping inside an item never moves the toolbar's focus. The embedded
2971
+ control's own value is not modelled: the host renders it and owns it.
2711
2972
  - **Divergences.** base-ui and ariakit skip disabled items in roving navigation, where `toolbar.qnt` keeps
2712
2973
  them focusable; the tab stop starts on item `0` even when that item is disabled, where the abstract
2713
2974
  Composite picks the first non-disabled item; and the primitive renders `role="toolbar"` with
@@ -2715,13 +2976,13 @@ createToolbar(options: {
2715
2976
  not this primitive.
2716
2977
  - **Limits (the source's own):** no separators, no RTL, and `items` does not grow after construction.
2717
2978
 
2718
- `Right` / `Left` on a horizontal toolbar and `Down` / `Up` on a vertical one move the roving tab stop along
2719
- the axis (`ArrowRight` and `Right` are normalised to one key), Home / End jump to the ends, and `wrap`
2720
- decides whether an edge wraps or stops. Enter and Space activate the focused `button` item, and both are
2721
- `preventDefault`ed. Tab from a `button` item leaves the toolbar (the browser moves on); from a `text` item it
2722
- steps to the neighbouring item and only leaves at the edge, which is how Tab escapes a text field inside a
2723
- composite. `focusIn`, `leave` and `tab(back)` are those steps as methods, and `focusItem`, `clickItem` and
2724
- `activate` are the pointer and programmatic paths.
2979
+ `Right` / `Left` on a horizontal toolbar and `Down` / `Up` on a vertical one move focus along the axis
2980
+ (`ArrowRight` and `Right` are normalised to one key), Home / End jump to the ends, and `wrap` decides whether
2981
+ an edge wraps or stops — all of it from the definition the owner's `focusgroup` attribute states. Enter and
2982
+ Space activate the focused `button` item, and both are `preventDefault`ed. Tab from a `button` item leaves the
2983
+ toolbar (the browser moves on); from a `text` item it steps to the neighbouring item and only leaves at the
2984
+ edge, which is how Tab escapes a text field inside a composite. `focusIn`, `leave` and `tab(back)` are those
2985
+ steps as methods, and `focusItem`, `clickItem` and `activate` are the pointer and programmatic paths.
2725
2986
 
2726
2987
  ### Headless: `createTextInput` and `createTextArea` (`@cssints/headless`)
2727
2988
 
@@ -2870,6 +3131,18 @@ and walks it differentially in thirteen configurations: single, follows-focus, a
2870
3131
  multi with and without one, type-ahead with `selectionMode: "none"`, and each of those controlled by
2871
3132
  a host that accepts every request and one that holds its value.
2872
3133
 
3134
+ The roving tab stop is the platform's `focusgroup="listbox"` contract (`shared/focusgroup.qnt`, the
3135
+ `focusGroup` adapter of the toolbar section): arrows along the block axis, no wrap, Home / End, the
3136
+ last-focused memory and the one tab stop are the browser's (Chromium 150+) or
3137
+ `@microsoft/focusgroup-polyfill`'s. The primitive renders the owner's `focusgroup` attribute and its
3138
+ `ref`, keeps every enabled option focusable (`tabindex="0"`, which the polyfill decorates into the
3139
+ stop and restores), and never decides which option is the stop. `focus` / `active` are readings of
3140
+ the DOM: an option's `focus` is the move, so the platform's arrow, a click and a host's own
3141
+ `element.focus()` land in the same state. The library keeps its delta: the selection model,
3142
+ `aria-selected`, range selection, type-ahead, and the option the arrows must not reach (it is out of
3143
+ the item collection and wears `focusgroup="none"`, because the platform reads only `aria-disabled`
3144
+ and would still move focus onto it).
3145
+
2873
3146
  ```ts skip
2874
3147
  // ListboxItemInput is a label string or { id?, label, textValue?, disabled? }
2875
3148
  createListbox(options: {
@@ -2886,6 +3159,7 @@ createListbox(options: {
2886
3159
  label?: string | undefined; // aria-label on the listbox
2887
3160
  onValueChange?(selected: ReadonlyArray<number>): void; // a request; onSelectionChange is an alias
2888
3161
  onActiveChange?(index: number): void; // a request
3162
+ provider?: "auto" | "polyfill"; // "auto": the feature detect; "polyfill" forces the low-level group
2889
3163
  }): {
2890
3164
  active(): number;
2891
3165
  selected(): ReadonlyArray<number>; // sorted, de-duplicated, in range
@@ -2894,7 +3168,7 @@ createListbox(options: {
2894
3168
  selectionMode: "single" | "multiple" | "none";
2895
3169
  focus(): number; // the option that takes focus on entry
2896
3170
  blur(): void;
2897
- keyDown(key: string, modifiers?: { shift?; ctrl?; meta?; alt? }): void;
3171
+ keyDown(key: string, modifiers?: { shift?; ctrl?; meta?; alt? }): void; // the library's keys, not the movement
2898
3172
  type(text: string): void;
2899
3173
  clearTypeahead(): void;
2900
3174
  clickOption(index: number): void;
@@ -2905,7 +3179,8 @@ createListbox(options: {
2905
3179
  "aria-multiselectable": "true" | "false" | undefined; // undefined in "none"
2906
3180
  "aria-activedescendant": string | undefined; // only while the widget holds focus
2907
3181
  "aria-label": string | undefined;
2908
- onKeyDown;
3182
+ focusgroup: string; // "listbox block nowrap"; "" when the polyfill is forced
3183
+ ref; // required: it builds the provider and the primitive's keydown listener
2909
3184
  onFocusOut;
2910
3185
  };
2911
3186
  optionProps(index: number): {
@@ -2915,7 +3190,9 @@ createListbox(options: {
2915
3190
  "aria-disabled": "true" | undefined;
2916
3191
  "aria-posinset";
2917
3192
  "aria-setsize";
2918
- tabindex: 0 | -1; // 0 on the active option only
3193
+ focusgroup: "none" | undefined; // "none" on an option the arrows must not reach
3194
+ tabindex: 0 | -1; // the constant focusability value: -1 only when disabled
3195
+ ref;
2919
3196
  onClick;
2920
3197
  onFocus;
2921
3198
  onMouseEnter;
@@ -2930,35 +3207,44 @@ not walk the whole list. A selection or a `defaultActive` chosen before the item
2930
3207
  applies once they do, because `selected()` normalizes against the live count at every read.
2931
3208
  Selection is a set of indices, always returned sorted and de-duplicated, and an option that is
2932
3209
  disabled either by the `disabled` index list or by its own `disabled` field is skipped by the
2933
- arrows, Home/End, clicks and hover.
3210
+ arrows, Home/End, clicks and hover: it is left out of the group's item collection and marked
3211
+ `focusgroup="none"`, so neither provider lands focus on it.
2934
3212
 
2935
3213
  Selection is uncontrolled by default (`defaultValue`, or `defaultActive` for the active option) and
2936
3214
  controlled by `value` + `onValueChange` (the active option by `activeValue` + `onActiveChange`,
2937
3215
  which is the type-ahead machine's value). Every selection change and every focus move is a request:
2938
3216
  uncontrolled it commits, controlled it commits nothing until the host writes the prop back, and a
2939
- request equal to the current state is not made at all.
3217
+ request equal to the current state is not made at all. A controlled active option that refuses a
3218
+ move also gets the DOM focus put back on the option the state names, because the platform has
3219
+ already moved it.
2940
3220
 
2941
3221
  **Keyboard.** In `"single"` and `"none"` a printable character is type-ahead: a fresh buffer
2942
3222
  searches from the option after the active one, wrapping, and a growing buffer searches from the
2943
3223
  active one inclusive, both on the item's `textValue` (its label by default); a Space continues a
2944
3224
  non-empty buffer instead of selecting. ArrowDown / ArrowUp move the active option without wrapping,
2945
- Home / End jump to the first / last enabled option, and Space or Enter selects the active option in
2946
- single-select (in `"none"` there is nothing to select). In `"multiple"` Space toggles the active
2947
- option, Shift+Space selects the run from the anchor to the active option, Shift+Arrow toggles the
2948
- option it enters, Ctrl+Shift+Home / End select to that edge, and Ctrl+A (or Meta+A) selects all, or
2949
- clears when all are selected. Ctrl+Space and Ctrl+Arrow are consumed and move nothing: the pattern's
2950
- alternative model, where the arrows move focus only, is not implemented. The type-ahead keys are
2951
- single- and none-only; `type(text)` itself searches in any mode.
3225
+ and Home / End jump to the first / last enabled option — the platform's movement, not the
3226
+ primitive's. Space or Enter selects the active option in single-select (in `"none"` there is nothing
3227
+ to select). In `"multiple"` Space toggles the active option, Shift+Space selects the run from the
3228
+ anchor to the active option, Shift+Arrow toggles the option it enters (the platform ignores the
3229
+ modifier, so the primitive moves the active option and the host's focus mirror follows), Ctrl+Shift+
3230
+ Home / End select to that edge, and Ctrl+A (or Meta+A) selects all, or clears when all are selected.
3231
+ Ctrl+Space and Ctrl+Arrow are consumed and move nothing: the pattern's alternative model, where the
3232
+ arrows move focus only, is not implemented. The type-ahead keys are single- and none-only;
3233
+ `type(text)` itself searches in any mode, and `keyDown(key)` drives exactly these library keys — an
3234
+ arrow or Home / End passed to it moves nothing, because the platform is the mover.
2952
3235
 
2953
3236
  There is no live region and no timer: the host calls `clearTypeahead()` from its own idle timer, and
2954
3237
  `focusOnHover` is off by default because moving the active option on hover fights a nested scroll or
2955
3238
  a tooltip. `focus()` names the entry option (the first selected one, else the first), returns the
2956
3239
  active one when the widget already has focus, and, in single-select with `followsFocus`, commits it.
2957
- `aria-activedescendant` is emitted while the widget holds focus, but the container itself has no
2958
- tabindex: the active option is the single tab stop, so the host keeps the DOM focus on that option.
2959
- The scenario divergences: `autoFocus` focuses the option, not the listbox, so a probe reads
2960
- `option:0`; and `onAction` / `onPress` are react-aria extensions the primitive does not report (only
2961
- the selection change).
3240
+ `aria-activedescendant` is emitted while the widget holds focus and the container itself has no
3241
+ tabindex: the rows are the focus holders, and the host moves the DOM focus onto the active row for a
3242
+ programmatic move (type-ahead, a click, a controlled `activeValue`); the platform's own moves need no
3243
+ mirror. A host that instead keeps DOM focus on the container — a combobox popup, or
3244
+ `createRearrangeableListbox` — is in the `aria-activedescendant` model, which is not a focusgroup and
3245
+ keeps its own arrow handling. The scenario divergences: `autoFocus` focuses the option, not the
3246
+ listbox, so a probe reads `option:0`; and `onAction` / `onPress` are react-aria extensions the
3247
+ primitive does not report (only the selection change).
2962
3248
 
2963
3249
  ### Headless: `createMeter` (`@cssints/headless`)
2964
3250
 
@@ -4111,6 +4397,74 @@ TOKENS
4111
4397
  - **Its own styles do not touch yours.** The overlay is a custom element with a shadow root and its own cssints sheet, built with the package: the page's rules do not reach it, and its classes are not in your sheet or in the table.
4112
4398
  - **Limits.** The proximity of `@scope` is not ranked (no two looks set one property on one element), and a logical and a physical property (`margin-inline-start`, `margin-left`) are two properties, as `cn()` treats them; an element inside another shadow root is picked as its host; a page that Vite does not serve as HTML (server rendering) gets no entry.
4113
4399
 
4400
+ #### Vitals in the console (`@cssints/devtools/vitals`, 2026-11-07, cssints-0p4d)
4401
+
4402
+ The panel inspects an element; this entry inspects the page, and prints to the console rather than drawing a DOM panel —
4403
+ the only surface a page that draws to a canvas has. No dev server, no overlay chunk and no cssints dependency of its own: a
4404
+ plain ES module (about 30 kB, 9.5 kB gzip) any page imports in dev.
4405
+
4406
+ ```ts
4407
+ // the app's entry, dev only
4408
+ import { observe, vitals } from "@cssints/devtools/vitals";
4409
+ observe(); // long tasks and long animation frames are not buffered: observe before the work
4410
+
4411
+ // later, from the console:
4412
+ vitals(); // or window.vitals(): prints the report and returns it
4413
+ ```
4414
+
4415
+ ```text skip
4416
+ cssints vitals
4417
+ 265 ms · 214 nodes · 1 sheet · 380 classes
4418
+ vitals FCP 412 ms · LCP 1.20 s (h1.hero) · CLS 0.002
4419
+ INP 96 ms (button.primary) · 8+40+48 · TTFB 38 ms
4420
+ frames p50 16.7 ms · p95 33.4 ms · max 51.2 ms · 3 dropped ▁▂▃▅▇▆▃▂▁▂▃▅
4421
+ main 7 long tasks · 412 ms · max 120 ms · style+layout 41 ms · 0 errors · heap 42.1 MB
4422
+ sheet 64.4 kB · +29 B on src/registry/button/button.tsx · 3 loads · 81 B a class ▁▁█
4423
+ load 2 sheets · 1 render-blocking · 66 ms to last · slow.css 65 ms (327 B)
4424
+ history 12 loads · LCP 1.20 s (−180 ms) (−312 B) ▁▂▃▅▆▇▆▅
4425
+ CSS 41.2 kB · 380 rules · 1 902 decls · 0 blocked · specificity 0,3,2 (._1t8gqy5:hover)
4426
+ layer _.k ████████████████████ 22.4 kB
4427
+ tokens 14 colours · 2 fonts · 1 !important · custom props 12 declared / 9 used / 3 orphaned · keyframes 2 / 1 unused
4428
+ coverage 41 / 380 classes unused (11%) cs-Landing-hero …
4429
+ ```
4430
+
4431
+ - **Web vitals** come from [web-vitals](https://github.com/GoogleChrome/web-vitals) (MIT), the reference implementation:
4432
+ CLS is the largest session window of shifts (not their sum), INP is the ~98th percentile of interactions (not the worst),
4433
+ and each metric names its culprit — the LCP element and render delay, the interaction's target with input / processing /
4434
+ presentation, the frames it covered. `!` is needs-improvement and `!!` poor, the library's thresholds. It reads the same
4435
+ APIs, so CLS is Chromium only and the other four come from Chromium, Firefox and Safari. Long animation frames are ours,
4436
+ and `render` sums the style and layout time — the part CSS pays for.
4437
+ - **The sheet's numbers** come from the CSSOM, not a parser: bytes, rules, declarations, layer bytes, the worst
4438
+ specificity, colours, fonts, `!important`, custom properties declared / read / orphaned / undeclared, keyframes declared
4439
+ / used / unused, and coverage — the classes of the sheet that no element carries now. On a cssints sheet this should be
4440
+ clean by construction, so a drift names what bypassed the engine; it is Project Wallace's x-ray pointed at a generated
4441
+ sheet.
4442
+ - **The sheet's growth** is the dev server's measurement, over the HMR channel (`cssints:edits`; a late subscriber asks
4443
+ with `cssints:edits:get`): each load reports its size, its class count and the file that changed, so an edit reads
4444
+ `+29 B on src/registry/button/button.tsx`, with a sparkline of the last thirty loads and the bytes a class costs. Off a
4445
+ dev server the row is absent. These bytes are `renderSheet`'s, not the CSSOM's, so they differ a little from the `CSS`
4446
+ row.
4447
+ - **Long animation frames** also name their scripts: the top sources by script time (a source is the last two path
4448
+ segments), the invokers (`timer`, `message handler`, an event listener), and the _forced_ style and layout time — the
4449
+ read-after-write that makes a script pay for layout.
4450
+ - **The history** of the last twenty loads of a path lives in `sessionStorage` (`cssints-vitals`): LCP and the sheet's
4451
+ bytes as deltas against the previous load of the same page; `reset()` clears it, and a store that throws (a private
4452
+ window) is simply absent.
4453
+ - **The cost by call** needs the devtools table (`cssints({ devtools: true })`): bytes of each class at the call that made
4454
+ it, the total and the number of calls, and how many of the call's classes the page carries; without the table the row is
4455
+ absent.
4456
+ - **The CSS files' arrival** is the resource timeline: each `.css`, its duration, Chrome's `renderBlockingStatus`, the
4457
+ slowest and the last.
4458
+ - **Frames** are sampled by a rAF loop: p50 / p95 / max, the dropped frames, a sparkline and a heatmap of the timeline
4459
+ (green under 16.7 ms, amber to 33.4, orange to 50, red above).
4460
+ - **Limits.** Long animation frames are not buffered entries, so a report from a cold console sees only the work after
4461
+ `observe()`; the five vitals are buffered but arrive a task after the module loads, so the first report can be early.
4462
+ A cross-origin sheet throws on `cssRules` and is counted as `blocked`. Coverage asks the DOM now: a class used only in a
4463
+ state is used if an element carries it.
4464
+
4465
+ `report()` returns the numbers, `print(report)` prints them, `reset()` forgets the history and the sheet's measurements,
4466
+ `stop()` ends the frame loop.
4467
+
4114
4468
  ### Debugging: traces of the engine (`CSSINTS_TRACE`, cssints-r2qq)
4115
4469
 
4116
4470
  Opt-in. Set `CSSINTS_TRACE=otlp` (or `otlp:http://host:port`; the default is `http://localhost:4318`) before `vite dev` or `vite build`, and the plugin sends one trace per Vite hook to an OpenTelemetry backend over OTLP/HTTP (JSON). Unset, nothing is loaded and nothing runs: the hooks use Effect's own `runSync` and `runPromiseExit`, no tracer, no exporter, no HTTP client. The variable is read once, when the plugin loads.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cssints",
3
- "version": "0.33.0",
3
+ "version": "0.35.0",
4
4
  "description": "CSS in TypeScript: typed css.* calls compiled to atomic CSS at build time (a Vite plugin)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -72,12 +72,12 @@
72
72
  }
73
73
  },
74
74
  "dependencies": {
75
- "@cssints/css-grammar": "^0.33.0",
75
+ "@cssints/css-grammar": "^0.35.0",
76
76
  "culori": "^4.0.2",
77
77
  "effect": "4.0.6",
78
- "happy-dom": "^20.14.5",
79
- "lightningcss": "^1.32.0",
80
- "magic-string": "^0.30.0",
78
+ "happy-dom": "^20.14.6",
79
+ "lightningcss": "^1.33.0",
80
+ "magic-string": "^0.30.21",
81
81
  "mdn-data": "2.27.1",
82
82
  "oxc-parser": "^0.149.0"
83
83
  },
@@ -114,7 +114,7 @@ shade: fn({
114
114
  - `value` runs at build time, once per call; the call is replaced by its CSS text. It throws for values the grammar
115
115
  cannot reject (units it cannot compute, a failed accessibility rule): `fluid` rejects `em` and fails WCAG 1.4.4.
116
116
  - What `value` returns must be a string or a Typed OM value (`cssints: tint() value must return a string or a Typed OM
117
- value`). A result that does not fit the property is that property's error at the same site.
117
+ value`). A result that does not fit the property is that property's error at the same site.
118
118
 
119
119
  ## Identity and HMR
120
120