@tanishqkancharla/maui 0.0.70 → 0.0.72

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.
@@ -225,7 +225,7 @@ const coloredButtonClass = memoize((variant, color) => {
225
225
  }
226
226
  const scale = paletteFill(color);
227
227
  if (variant === "primary") {
228
- return primaryClass(scale[9], darkTextOnSolid.has(color) ? scale[12] : "white", paletteAlpha(color)[6]);
228
+ return primaryClass(scale[9], darkTextOnSolid.has(color) ? onSolidText(scale[9]) : "white", paletteAlpha(color)[6]);
229
229
  }
230
230
  return quietClass(scale[11], scale[12], variant === "quiet" ? paletteAlpha(color)[9] : undefined);
231
231
  });
@@ -32,9 +32,12 @@ const item = style(spacing.padding({ x: 4, y: 2 }), text({ size: "sm", fontWeigh
32
32
  background: backgroundColor.elementHover,
33
33
  },
34
34
  "&[data-selected]": {
35
- color: colors.accent[9],
35
+ color: colors.accent[11],
36
36
  fontWeight: 500,
37
37
  },
38
+ "&[data-selected]:not([data-disabled]):is(:hover, [data-hovered], [data-focused])": {
39
+ color: colors.accent[12],
40
+ },
38
41
  "&[data-selected]::after": {
39
42
  content: '"✓"',
40
43
  position: "absolute",
@@ -69,7 +69,7 @@ const row = style({
69
69
  "&[data-selection-mode][data-selected] td": {
70
70
  backgroundColor: colors.accent[3],
71
71
  },
72
- "&[data-disabled]": {
72
+ "&[data-disabled] td": {
73
73
  color: colors.gray[8],
74
74
  },
75
75
  });
@@ -105,11 +105,11 @@ const selectionCheck = style({
105
105
  height: "10px",
106
106
  paddingTop: "0.5px",
107
107
  paddingLeft: "0.5px",
108
- fill: colors.gray[3],
108
+ fill: "white",
109
109
  opacity: 0,
110
110
  transform: "scale(0)",
111
111
  });
112
- const selectionCheckbox = style(flex({ alignItems: "center", justifyContent: "center" }), focusRing(`&[data-focus-visible] ${selectionBox}`, shadowVars.control), {
112
+ const selectionCheckbox = style(flex({ alignItems: "center", justifyContent: "center" }), focusRing(`&[data-focus-visible] ${selectionBox}`, shadowVars.control), focusRing(`&[data-selected][data-focus-visible] ${selectionBox}, &[data-indeterminate][data-focus-visible] ${selectionBox}`, shadowVars.selectedControl), {
113
113
  position: "relative",
114
114
  width: "fit-content",
115
115
  cursor: "default",
@@ -129,6 +129,7 @@ const selectionCheckbox = style(flex({ alignItems: "center", justifyContent: "ce
129
129
  },
130
130
  [`&[data-selected] ${selectionBox}, &[data-indeterminate] ${selectionBox}`]: {
131
131
  backgroundColor: colors.accent[9],
132
+ boxShadow: shadowVars.selectedControl,
132
133
  },
133
134
  [`&[data-hovered][data-selected] ${selectionBox}, &[data-hovered][data-indeterminate] ${selectionBox}`]: {
134
135
  backgroundColor: surfaceWash(colors.gray[12], surfaceMixPercent.hover, colors.accent[9]),
@@ -13,7 +13,10 @@ export const navigationItem = style(spacing.padding({ x: 4, y: 2 }), text({ size
13
13
  background: backgroundColor.elementHover,
14
14
  },
15
15
  "&[aria-current='page']": {
16
- color: colors.accent[9],
16
+ color: colors.accent[11],
17
17
  fontWeight: 500,
18
18
  },
19
+ "&[aria-current='page']:hover": {
20
+ color: colors.accent[12],
21
+ },
19
22
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanishqkancharla/maui",
3
- "version": "0.0.70",
3
+ "version": "0.0.72",
4
4
  "description": "Maui design system",
5
5
  "type": "module",
6
6
  "main": "./dist/maui.js",
@@ -29,16 +29,16 @@ Styled React Aria `Button` (default `type="button"`). Consumes `ButtonContext`,
29
29
  | `variant` | `"default"` (raised element) \| `"quiet"` (transparent, hover wash) \| `"text"` (transparent, color-only hover) \| `"primary"` (solid fill) |
30
30
  | `variantColor` | Palette name or opaque `#hex` / `rgb(...)`. Primary defaults to `"accent"`; quiet and text default to gray and tint the label/icon when set |
31
31
  | `isDisabled` | React Aria name. Maps to native `disabled`. **No `disabled` React prop** |
32
- | `isPending` | Shows a current-color indicator beside visible content: `xs` (14px) by default, `2xs` (12px) for small buttons, with equal leading/top/bottom insets. Crossfades content and resizes over 150ms in both directions; reduced motion switches instantly. Blocks activation immediately and suppresses hover fill, retaining focusability and variant color. Controlled by the caller, not by returned promises |
32
+ | `isPending` | Shows a current-color indicator beside visible content. Blocks activation and suppresses hover fill while retaining focusability and variant color. Controlled by the caller, not by returned promises |
33
33
  | `pendingLabel` | Optional visible string replacing children while pending (e.g. `"Saving…"`). Omit to keep children; provide it to replace an icon with progress text |
34
34
  | `onClick` | Still supported. React Aria also exposes `onPress` |
35
35
  | `children` | Text is wrapped for cap-height trim; SVG icons sit beside text. Icon-only needs `aria-label` |
36
36
 
37
- Primary: fill step 9, light text (`onAccent`, or step 12 on amber/lime/mint/sky/yellow). Custom CSS fill drops alpha; text is white or near-black from lightness. The fill is a diagonal gradient (`to top left`) from the fill shifted by OKLCH `l − 0.027`, `c × 1.05`, `h − 6.8` to the fill itself. In dark mode the shadow has a 0.5px white top highlight at 15% (`shadow.control` uses 9%); light mode has none. The shadow tints a soft lift from the fill, at half strength in dark mode, and adds a 0.5px edge painted beneath the lift: the fill color’s alpha step 6 in light mode (a custom CSS fill uses the fill at 35%); in dark mode a near-black tint of the fill (OKLCH lightness 0.18, half chroma) — no neutral gray. Hover layers a white-to-transparent sheen over the gradient; disabled drops the gradient. Quiet: no fill; `variantColor` tints the label/icon. Hover/press mix `grayAlpha[9]` (or that color’s alpha 9, or the hex) into transparent at 6% light / 9% dark; press is 2×. Hover fills snap.
37
+ Use primary for an emphasized action, default for a raised neutral control, quiet for a low-emphasis action, and text for an inline-style action without a hover fill. Let Button manage its palette and state colors rather than rebuilding the fill or shadow. For exact treatment, inspect `src/components/Button.tsx` in the installed package as described in [design conventions](../design_conventions.md).
38
38
 
39
- While a menu or overlay is open, the trigger keeps the active fill via RAC `data-pressed` / `aria-expanded` (same tokens as `:active`: `elementActive`, or primary step 10).
39
+ Prefer plain labels (optionally alongside icons) so Button controls their foreground; see the [composition rule](../design_conventions.md#composition).
40
40
 
41
- Text: zero padding in both sizes, including icons and pending content; retains the control height and focus ring. No background or resting shadow. Only the label/icon color changes on hover (palette step 11 → 12, or a darker custom color). Disabled and pending suppress hover changes.
41
+ The text variant removes horizontal spacing and surface chrome while keeping the control height and focus ring. Use it when an action should align directly with surrounding text; hover changes the foreground, not the fill.
42
42
 
43
43
  `useButton(props)` is exported for custom focus-tracked buttons; prefer `Button`.
44
44
 
@@ -23,7 +23,7 @@ Exactly two children on `MenuTrigger`: **trigger**, then **menu**. Use Maui `But
23
23
  </MenuTrigger>
24
24
  ```
25
25
 
26
- `isDisabled` on the trigger button blocks open. Escape closes and focus returns to the trigger (`aria-expanded` tracks open state). While open, the Maui `Button` keeps its active fill from RAC `data-pressed` / `aria-expanded`.
26
+ `isDisabled` on the trigger button blocks open. Escape closes and focus returns to the trigger (`aria-expanded` tracks open state).
27
27
 
28
28
  `placement` defaults to `"bottom start"`. Popover uses `shadow.strong` via `CollectionPopover`.
29
29
 
@@ -1,6 +1,6 @@
1
1
  # Maui design conventions
2
2
 
3
- Prefer the Maui skill for composition knowledge: how to assemble tokens, components, patterns, and apps. Read every matching reference linked from `SKILL.md` before designing or implementing UI, and reuse its structure, tokens, and interactions. Reach for package source under `"maui/src/patterns"`, `"maui/src/apps"`, or an installed `node_modules` package only when the references do not cover a needed detail.
3
+ Use the matching skill references for Maui's design choices, composition rules, and reusable token patterns. Inspect the installed package's source for tactical questions about props, defaults, state styling, or exact values. Source ships under `node_modules/maui/src/` with the documented npm alias, or `node_modules/@tanishqkancharla/maui/src/` without it. Follow components into their token definitions as needed; current implementation is not a guarantee of contrast or design intent.
4
4
 
5
5
  Patterns, demo apps, and the gallery `Panel` preview frame are not package components. `Panel` is not on the `"maui"` barrel, in `src/components`, or in the JSX editor catalog. Rebuild reference recipes with barrel exports such as `Button`, `Flex`, and `text(...)`.
6
6
 
@@ -24,30 +24,59 @@ State styles key off React Aria attributes (`data-hovered`, `data-selected`, `da
24
24
 
25
25
  ## Composition
26
26
 
27
- - Use Maui's `Text` component instead of raw `<span>` elements for inline text. Set typography through its `size`, `fontWeight`, `color`, `monospace`, and `tabular` props; see [Text](components/text.md). Keep semantic elements such as headings, paragraphs, labels, and links for their respective roles.
27
+ - Use Maui's `Text` component for standalone inline typography; see [Text](components/text.md). `Text` and `text(...)` set an explicit color by default, overriding inherited variant, selected, or disabled colors. Prefer plain Button labels so the Button controls its foreground. Keep semantic elements such as headings, paragraphs, labels, and links for their respective roles.
28
28
  - Hover backgrounds have no transitions. Hover fills such as `backgroundColor.elementHover`, quiet-button washes, and list or row highlights snap instantly. Do not animate `background` or `background-color` on hover with `motion.standard(...)` or a CSS `transition`. Other motion, including tooltips and transforms, is allowed.
29
29
  - Simple apps default to a prose column: `style(proseContainerStyle, { width: "100%", marginInline: "auto" })`. `sizing.contentWidth` is that same style. Use it for single-column tools, settings, forms, and reading layouts. Multi-pane or full-bleed apps such as inboxes, calendars, and IDEs are exceptions.
30
30
  - Design an intentional empty composition for every list, inbox, search result, or collection. Include copy and, when useful, an action. Do not leave a blank panel.
31
31
 
32
+ ## Decorative edge strips
33
+
34
+ When a rounded surface uses a colored edge for emphasis, use a full-height rectangular strip inside a `position: relative; overflow: hidden` container. Keep the radius on the container, not the strip: a CSS left border follows the corners and curls inward. This is a composition recipe for custom UI, not a new component or a change to existing component borders. Keep using `border()` for outlines and dividers.
35
+
36
+ ```tsx
37
+ const surface = style(radius.md, spacing.padding({ all: 6 }), {
38
+ position: "relative",
39
+ overflow: "hidden",
40
+ backgroundColor: colors.orange[3],
41
+ })
42
+ const strip = style({
43
+ position: "absolute",
44
+ insetBlock: 0,
45
+ left: 0,
46
+ width: "3px",
47
+ backgroundColor: colors.orange[9],
48
+ pointerEvents: "none",
49
+ })
50
+
51
+ function DecoratedSurface() {
52
+ const surfaceClass = useStyles(surface)
53
+ const stripClass = useStyles(strip)
54
+ return (
55
+ <div className={surfaceClass}>
56
+ <div aria-hidden="true" className={stripClass} />
57
+ <Text>Surface content</Text>
58
+ </div>
59
+ )
60
+ }
61
+ ```
62
+
32
63
  ## Shadows
33
64
 
34
- Use `shadow.control` for interactive control surfaces. It combines two inset
35
- highlights with a compact outer edge. Use the three-level elevation scale for
36
- other raised surfaces:
65
+ Use `shadow.control` for interactive control surfaces. Use the three-level elevation scale for other raised surfaces:
37
66
 
38
67
  - `shadow.subtle` — cards and ordinary raised surfaces
39
- - `shadow.medium` — tooltips and larger floating panels
68
+ - `shadow.medium` — larger floating panels
40
69
  - `shadow.strong` — dropdowns, popovers, and dominant overlays
41
70
 
42
- All three include a 1px ring. Do not also apply `border()`, `borderColor.outline`, or another ring to the same element.
71
+ These shadows include their own edge or ring. Do not also apply `border()`, `borderColor.outline`, or another ring to the same element. Not every floating component uses elevation: reuse its exported styles rather than inferring a shadow from its placement.
43
72
 
44
73
  Buttons and form-control surfaces use `shadow.control` by default. For compound controls, apply it once to the outer control boundary instead of to every internal button or segment.
45
74
 
46
75
  ## Focus
47
76
 
48
- `focusRing()` applies a theme-aware Radix blue shadow with a `blueAlpha[8]` hard edge and `blueAlpha[5]` glow. Do not add a competing outline or box shadow for keyboard focus.
77
+ Use `focusRing()` for Maui's theme-aware keyboard-focus treatment rather than adding a competing outline or box shadow.
49
78
 
50
- **Control safe area.** Input and form-control shadows paint outside the component's layout box; reserve room for the resting, hovered, and keyboard-focused states, even when hover does not change the background. The focus treatment includes a 1px ring and a 6px blur. Use `spacing.padding(4)` (9px) as a practical safe inset between controls and clipping edges, and leave gaps between neighboring controls. Keep overflow visible on immediate wrappers; when a panel, rounded container, or scroll viewport must clip, put the safe inset inside that clipping boundary, not outside it as margin. Check the first and last controls at scroll edges, including checkbox, radio, switch, and slider focus states. Do not remove shadows or focus rings to fit a tight layout.
79
+ **Control safe area.** Input and form-control shadows paint outside the component's layout box; reserve room for the resting, hovered, and keyboard-focused states, even when hover does not change the background. Use `spacing.padding(4)` as a starting inset between controls and clipping edges, and leave gaps between neighboring controls. Keep overflow visible on immediate wrappers; when a panel, rounded container, or scroll viewport must clip, put the safe inset inside that clipping boundary, not outside it as margin. Check the first and last controls at scroll edges, including checkbox, radio, switch, and slider focus states. Do not remove shadows or focus rings to fit a tight layout.
51
80
 
52
81
  ```tsx
53
82
  // Keep the control's visual overflow inside the scrolling boundary.
@@ -61,7 +90,7 @@ Buttons and form-control surfaces use `shadow.control` by default. For compound
61
90
 
62
91
  ## Layout utilities
63
92
 
64
- `Flex` and `Gap` take spacing scale steps (`1 | 2 | 3 | 4 | 6 | 8 | 12 | 16`), not raw pixels. For example, `<Flex row gap={4} padding={4}>` has a 9px gap and 9px inset.
93
+ `Flex` and `Gap` take spacing scale steps, not raw pixels. For example, `<Flex row gap={4} padding={4}>` uses the same token for its gap and inset.
65
94
 
66
95
  Put padding on `Flex` with `padding` or `p`, `px`, `py`, `pt`, `pr`, `pb`, and `pl`; there is no `Padding` component. `alignItems` and `justifyContent` use the same tokens as `flex()` (`start`, `center`, `end`, and others; `justifyContent` also supports `between`, `around`, and `evenly`). `background` is a surface token such as `app`, `element`, or `accent`. `Spacer` grows to fill leftover flex space. `Divider` is a horizontal rule.
67
96
 
@@ -56,12 +56,14 @@ function Row({ children }: { children: React.ReactNode }) {
56
56
 
57
57
  `colors` is Radix Scales with a brand `accent` (teal in light, violet in dark). Each scale has steps **1–12**. Alpha scales are `colors.grayAlpha`, `colors.accentAlpha`, `colors.blueAlpha`, …
58
58
 
59
+ The scale roles below are a starting point, not a contrast guarantee or a state recipe for every component. Use text roles for labels rather than copying a solid fill's step. Check the actual foreground/background pair in light and dark themes, including selected and disabled states; preserve non-color cues for state and status.
60
+
59
61
  | Steps | Use |
60
62
  | --- | --- |
61
63
  | 1–2 | App / subtle surfaces |
62
64
  | 3–5 | Interactive washes |
63
65
  | 6–8 | Borders / strong lines |
64
- | 9–10 | Solid fills (9 rest, 10 hover) |
66
+ | 9–10 | Solid fills |
65
67
  | 11–12 | Text (11 secondary, 12 primary) |
66
68
 
67
69
  ```ts
@@ -74,6 +76,8 @@ colors.blueAlpha[8]
74
76
 
75
77
  `colorNames` is `"accent"` plus every Radix palette name. `paletteNames` omits `"accent"`. `"blue"` as a `variantColor` is the Radix blue scale, not CSS `blue`.
76
78
 
79
+ Brand accent, semantic status, and decorative identity serve different roles. Prefer a component's variant or tone before assembling custom colors; follow the matching component's source when composing similar UI. For example, [Banner](components/banner.md) owns status tones, while [Avatar](components/avatar.md) uses paired decorative colors, not status meanings.
80
+
77
81
  Prefer semantic surfaces over raw scale steps for chrome:
78
82
 
79
83
  ```ts
@@ -81,14 +85,14 @@ import { background, backgroundColor } from "maui"
81
85
 
82
86
  backgroundColor.app // page
83
87
  backgroundColor.element // raised control / card
84
- backgroundColor.elementHover // 3.5% gray-12 wash over element
85
- backgroundColor.elementActive // 7% wash
88
+ backgroundColor.elementHover // hovered element surface
89
+ backgroundColor.elementActive // active element surface
86
90
  background.app // style objects of the same values
87
91
  background.element
88
92
  background.accent // accent[9]
89
93
  ```
90
94
 
91
- Hover/active washes use `color-mix` in oklch (`surfaceWash`, transparent by default). **Do not transition `background` / `background-color` on hover.** Quiet `Button` uses `surfaceMixPercent` (6% light / 9% dark, press 2×) with Alpha[9]. Raised `elementHover` is still 3.5% gray-12 over the element surface.
95
+ Alpha washes depend on their backdrop. A quiet control's transparent overlay is not interchangeable with `elementHover`, which is precomposed over the element surface. Reuse the matching component styles rather than recreating its wash. Hover fills snap instantly; see [design conventions](design_conventions.md#composition).
92
96
 
93
97
  ## Text
94
98
 
@@ -115,7 +119,7 @@ text({
115
119
 
116
120
  Sizes step up on the **Large platform scale** (`:root[data-scale="large"]`, driven by `(pointer: coarse)` under the system preference): `md` is 14px/22px on desktop and 17px/26px on coarse pointers. See `useScale` / `ScaleProvider`.
117
121
 
118
- - `lowContrast` → `gray[11]`, `highContrast` → `gray[12]`, `accent` → `accent[11]`, `onAccent` → white.
122
+ - `lowContrast` → `gray[11]`, `highContrast` → `gray[12]`, `accent` → `accent[11]`, `onAccent` → white. `onAccent` is literal white, not an adaptive foreground for every solid fill.
119
123
  - UI sans: `fontFamily` (system ui-sans-serif stack). Mono: Commit Mono with `ss05` smart kerning (`monoFontStyle` / `monospace`).
120
124
  - `tabular: true` → `font-variant-numeric: tabular-nums`. NumberField, TableHead, and TableCell turn this on by default. Body copy and Prose stay off.
121
125
  - `baseTextStyle` is md / 400 / highContrast — already on `html, body`.
@@ -178,13 +182,12 @@ shadowVars.control | .subtle | .medium | .strong // raw box-shadow strings
178
182
  tintedSubtle(color) // subtle ring+blur tinted from a fill
179
183
  ```
180
184
 
181
- `shadow.control` is a small drop over a 0.5px edge: gray A6 (`grayAlpha[6]`) in light mode, 80% black in dark mode so the control looks cut into the surface. Dark mode also adds a 0.5px top highlight. Every elevation
182
- already includes a 1px ring. **Do not also `border()` the same element.**
185
+ For elevation choice, compound control boundaries, and avoiding doubled rings, follow the [shadow conventions](design_conventions.md#shadows).
183
186
 
184
187
  `border(sides, color)`:
185
188
 
186
189
  - `sides`: `[]` for all sides, or `"top" | "right" | "bottom" | "left"`
187
- - `color`: `"border"` (gray-12 @ 5%) | `"outline"` (10%) | `"accent"` (`accent[8]`)
190
+ - `color`: `"border"` (subtle separator) | `"outline"` (stronger edge) | `"accent"` (accent edge)
188
191
 
189
192
  `borderColor.border` / `borderColor.outline` are the raw color strings (hairlines, `borderBottom`).
190
193
 
@@ -195,20 +198,14 @@ already includes a 1px ring. **Do not also `border()` the same element.**
195
198
  ## Motion
196
199
 
197
200
  ```ts
198
- motionDurationMs // 80
199
- motionStreamDurationMs // 80 — Streamdown word fade
200
- motionEasing // "ease-in-out"
201
- motion.standard("opacity", "transform") // 80ms ease-in-out on those properties
202
- overlayMotion // shared tooltip, dialog, backdrop, and reduced timings
201
+ motion.standard("opacity", "transform") // shared transition style
202
+ motionDurationMs // standard duration
203
+ motionStreamDurationMs // streaming text duration
204
+ motionEasing // standard easing
205
+ overlayMotion // overlay and reduced-motion presets
203
206
  ```
204
207
 
205
- Use `motion.standard` for opacity, transform, box-shadow, color. **Never** for hover `background` / `background-color`.
206
-
207
- `overlayMotion` defines tooltip enter (120ms entrance curve), tooltip exit
208
- (80ms ease-in), dialog enter (200ms entrance curve), dialog exit (120ms
209
- ease-in), backdrop (100ms), and opacity-only reduced motion (80ms). Crossfade
210
- enter is a 0.3s spring (`bounce` 0.2); exit uses `motionDurationMs` /
211
- ease-in-out. Streaming markdown uses `motionStreamDurationMs`.
208
+ Use `motion.standard` for opacity, transform, box-shadow, and color, not hover fills; see [design conventions](design_conventions.md#composition). Reuse `overlayMotion` for custom overlay enter/exit and reduced-motion handling; inspect the matching component for how it applies those presets.
212
209
 
213
210
  ## Sizing and icons
214
211
 
@@ -229,7 +226,7 @@ Stroke/fill is `currentColor`. Pass `size` on the icon component; optional `widt
229
226
 
230
227
  ## Avatar tokens
231
228
 
232
- `avatar.green` / `avatar.orange` / `avatar.pink` each have `background` and `foreground`. The `Avatar` component also uses `accent[4]`/`[11]`. You rarely need these tokens directly.
229
+ `avatar.green` / `avatar.orange` / `avatar.pink` each provide a paired `background` and `foreground`; keep the pair together when using these decorative tokens. Prefer `Avatar` for identity UI rather than choosing its colors manually.
233
230
 
234
231
  ## Prose tokens
235
232
 
@@ -1463,7 +1463,7 @@ const miniDayClass = style(
1463
1463
  text({ size: "xs", fontWeight: 400, color: "highContrast" }),
1464
1464
  radius.circle,
1465
1465
  focusRing(),
1466
- motion.standard("background-color", "color"),
1466
+ motion.standard("color"),
1467
1467
  {
1468
1468
  display: "grid",
1469
1469
  placeItems: "center",
@@ -303,7 +303,7 @@ const coloredButtonClass = memoize(
303
303
  if (variant === "primary") {
304
304
  return primaryClass(
305
305
  scale[9],
306
- darkTextOnSolid.has(color) ? scale[12] : "white",
306
+ darkTextOnSolid.has(color) ? onSolidText(scale[9]) : "white",
307
307
  paletteAlpha(color)[6],
308
308
  )
309
309
  }
@@ -41,9 +41,12 @@ const item = style(
41
41
  background: backgroundColor.elementHover,
42
42
  },
43
43
  "&[data-selected]": {
44
- color: colors.accent[9],
44
+ color: colors.accent[11],
45
45
  fontWeight: 500,
46
46
  },
47
+ "&[data-selected]:not([data-disabled]):is(:hover, [data-hovered], [data-focused])": {
48
+ color: colors.accent[12],
49
+ },
47
50
  "&[data-selected]::after": {
48
51
  content: '"✓"',
49
52
  position: "absolute",
@@ -111,7 +111,7 @@ const row = style({
111
111
  "&[data-selection-mode][data-selected] td": {
112
112
  backgroundColor: colors.accent[3],
113
113
  },
114
- "&[data-disabled]": {
114
+ "&[data-disabled] td": {
115
115
  color: colors.gray[8],
116
116
  },
117
117
  })
@@ -172,7 +172,7 @@ const selectionCheck = style({
172
172
  height: "10px",
173
173
  paddingTop: "0.5px",
174
174
  paddingLeft: "0.5px",
175
- fill: colors.gray[3],
175
+ fill: "white",
176
176
  opacity: 0,
177
177
  transform: "scale(0)",
178
178
  })
@@ -180,6 +180,10 @@ const selectionCheck = style({
180
180
  const selectionCheckbox = style(
181
181
  flex({ alignItems: "center", justifyContent: "center" }),
182
182
  focusRing(`&[data-focus-visible] ${selectionBox}`, shadowVars.control),
183
+ focusRing(
184
+ `&[data-selected][data-focus-visible] ${selectionBox}, &[data-indeterminate][data-focus-visible] ${selectionBox}`,
185
+ shadowVars.selectedControl,
186
+ ),
183
187
  {
184
188
  position: "relative",
185
189
  width: "fit-content",
@@ -201,6 +205,7 @@ const selectionCheckbox = style(
201
205
  [`&[data-selected] ${selectionBox}, &[data-indeterminate] ${selectionBox}`]:
202
206
  {
203
207
  backgroundColor: colors.accent[9],
208
+ boxShadow: shadowVars.selectedControl,
204
209
  },
205
210
  [`&[data-hovered][data-selected] ${selectionBox}, &[data-hovered][data-indeterminate] ${selectionBox}`]:
206
211
  {
@@ -18,8 +18,11 @@ export const navigationItem = style(
18
18
  background: backgroundColor.elementHover,
19
19
  },
20
20
  "&[aria-current='page']": {
21
- color: colors.accent[9],
21
+ color: colors.accent[11],
22
22
  fontWeight: 500,
23
23
  },
24
+ "&[aria-current='page']:hover": {
25
+ color: colors.accent[12],
26
+ },
24
27
  },
25
28
  )
@@ -36,11 +36,11 @@ const borderExamples = [
36
36
  use: "Pane headers and horizontal section breaks.",
37
37
  },
38
38
  {
39
- label: 'border(["left"], "accent") + borderLeftWidth',
39
+ label: "Clipped left accent",
40
40
  sides: ["left"] as BorderSide[],
41
41
  color: "accent" as const,
42
42
  edgeWidth: "2px",
43
- use: "Blockquotes and left-edge emphasis bars.",
43
+ use: "Decorative emphasis on rounded surfaces.",
44
44
  },
45
45
  ] as const
46
46
 
@@ -119,17 +119,46 @@ export function BordersTokenPage() {
119
119
  </TableBody>
120
120
  </Table>
121
121
 
122
- <CodeBlock lang="typescript">{`const panel = style(
122
+ <CodeBlock lang="tsx">{`const panel = style(
123
123
  background.element,
124
124
  border([], "outline"),
125
125
  radius.md,
126
126
  )
127
127
 
128
- const blockquote = style(
129
- border(["left"], "accent"),
130
- { borderLeftWidth: "2px" },
131
- spacing.padding({ left: 12 }),
132
- )`}</CodeBlock>
128
+ const accentedSurface = style(
129
+ radius.md,
130
+ {
131
+ position: "relative",
132
+ overflow: "hidden",
133
+ padding: "16px",
134
+ backgroundColor: colors.gray[2],
135
+ },
136
+ )
137
+ const strip = style({
138
+ position: "absolute",
139
+ insetBlock: 0,
140
+ left: 0,
141
+ width: "2px",
142
+ backgroundColor: colors.accent[8],
143
+ pointerEvents: "none",
144
+ })
145
+
146
+ function DecoratedSurface() {
147
+ const surfaceClass = useStyles(accentedSurface)
148
+ const stripClass = useStyles(strip)
149
+ return (
150
+ <div className={surfaceClass}>
151
+ <div aria-hidden="true" className={stripClass} />
152
+ <Text>Surface content</Text>
153
+ </div>
154
+ )
155
+ }`}</CodeBlock>
156
+ <P>
157
+ For left-edge emphasis, clip a full-height rectangular accent inside
158
+ the rounded container instead of using a left border. The container
159
+ keeps its rounding without curling the accent inward. This is a
160
+ composition recipe, not a change to existing component borders.
161
+ </P>
133
162
 
134
163
  <Panel style={{ marginTop: "16px" }}>
135
164
  <div
@@ -166,20 +195,13 @@ function BorderExample(props: {
166
195
  color: BorderColor
167
196
  edgeWidth?: string
168
197
  }) {
198
+ const leftAccent = props.sides.length === 1 && props.sides[0] === "left"
169
199
  const className = useStyles(
170
- border(props.sides, props.color),
171
- props.edgeWidth
172
- ? style(
173
- Object.fromEntries(
174
- props.sides.map((side) => [
175
- `border${capitalize(side)}Width`,
176
- props.edgeWidth,
177
- ]),
178
- ),
179
- )
180
- : undefined,
200
+ !leftAccent && border(props.sides, props.color),
181
201
  radius.md,
182
202
  style({
203
+ position: "relative",
204
+ overflow: "hidden",
183
205
  background: colors.gray[2],
184
206
  padding: "16px",
185
207
  textAlign: "center",
@@ -190,15 +212,26 @@ function BorderExample(props: {
190
212
 
191
213
  return (
192
214
  <div>
193
- <div className={className}>{props.label}</div>
215
+ <div className={className}>
216
+ {leftAccent && (
217
+ <div
218
+ aria-hidden="true"
219
+ style={{
220
+ position: "absolute",
221
+ insetBlock: 0,
222
+ left: 0,
223
+ width: props.edgeWidth ?? "1px",
224
+ backgroundColor: colors.accent[8],
225
+ pointerEvents: "none",
226
+ }}
227
+ />
228
+ )}
229
+ {props.label}
230
+ </div>
194
231
  </div>
195
232
  )
196
233
  }
197
234
 
198
- function capitalize(value: string) {
199
- return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`
200
- }
201
-
202
235
  function SingleEdgeExample() {
203
236
  const className = useStyles(
204
237
  border(["bottom"], "border"),
@@ -142,7 +142,7 @@ const itemClass = style(navigationItem, focusRing("&:focus-visible"), {
142
142
  backgroundColor: "transparent",
143
143
  color: colors.gray[12],
144
144
  "&[aria-current='page']": {
145
- color: colors.accent[9],
145
+ color: colors.accent[11],
146
146
  fontWeight: 500,
147
147
  backgroundColor: "transparent",
148
148
  },