@blakesteve/roster 4.8.0 → 4.8.2

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/README.md CHANGED
@@ -152,6 +152,12 @@ makes no difference, because the portal is still a descendant. If you scope
152
152
  the field and carries it across the portal, so the menu follows the same scope
153
153
  the trigger is in.
154
154
 
155
+ That mechanism carries the `.dark` CLASS across the portal. It does not carry
156
+ custom properties: `--roster-popover-*` and any other variable you scope to a
157
+ container reach the trigger and stop at the portal boundary, because the panel
158
+ is not a descendant of your container. Set those at `:root`. See
159
+ [Floating surface colors](#floating-surface-colors).
160
+
155
161
  `Navbar`'s `themeMode` prop is a different thing: it describes what palette the
156
162
  bar paints _itself_ with. Pair them with `themeMode="auto"` and the nav follows
157
163
  whatever `ThemeToggle` sets.
@@ -254,6 +260,9 @@ Note also that a solid fill's contrast against the surface _behind_ it is a
254
260
  separate requirement (WCAG 1.4.11, 3:1) and is not covered here. Solid amber on
255
261
  a white page is 1.67:1, so it has effectively no visible edge.
256
262
 
263
+ Form controls **are** covered for that requirement, separately: see
264
+ `--roster-control-border` under [Control surface colors](#control-surface-colors).
265
+
257
266
  ### Focus
258
267
 
259
268
  Every focusable control draws the same ring: `--roster-ring` for the ring
@@ -262,6 +271,24 @@ control's own fill. Both flip with the theme — `primary-500` on white in light
262
271
  `primary-400` on `gray-950` in dark — which puts the indicator at 6.37:1 and
263
272
  7.31:1 against the surface either side of it, where WCAG 1.4.11 asks for 3:1.
264
273
 
274
+ Both tokens follow the **page**, which is right until a component deliberately
275
+ inverts against it. `Dialog`'s `slate` and `primary` are dark whatever the page
276
+ is doing, so on a light page they were handing their contents the light-mode
277
+ ring: `primary-500` on `gray-700` is **1.61:1**, and the white offset behind it
278
+ was 10.27:1, making the gap the most visible part of the indicator. Those two
279
+ variants now set both tokens on their own subtree, so a field inside them gets
280
+ 3.80:1 and 4.04:1 instead. `white` and `glass` are left alone because they
281
+ follow the page like everything else.
282
+
283
+ If you build your own inverted surface, do the same on it:
284
+
285
+ ```css
286
+ .my-dark-panel {
287
+ --roster-ring: var(--roster-primary-400);
288
+ --roster-ring-offset: /* that panel's own background */;
289
+ }
290
+ ```
291
+
265
292
  Retint it for your own accent:
266
293
 
267
294
  ```css
@@ -359,40 +386,55 @@ pseudo-elements for any element that sets either of them, so on anything modern
359
386
 
360
387
  ### Control surface colors
361
388
 
362
- `Input`, `PasswordInput` and `Select` read these variables in their `outline`
363
- variant, so a consumer can repaint a form control without restating the class
364
- list.
389
+ `Input`, `PasswordInput`, `Textarea` and `Select` read these variables, so a
390
+ consumer can repaint a form control without restating the class list. Both
391
+ `outline` and `soft` read the border: `soft` used to be borderless and lean on
392
+ its fill alone, which fails the moment the fill matches the surface — a `soft`
393
+ field inside a `white` Dialog in dark mode was exactly 1.00:1 and invisible
394
+ until focus.
365
395
 
366
396
  ```css
367
397
  :root {
368
- --roster-control-border: #d6d3d1;
398
+ --roster-control-border: #65635f; /* gray-500 */
369
399
  --roster-control-bg: transparent;
370
400
  --roster-control-text: #1c1917;
371
401
  --roster-control-border-focus: #0f6498;
372
402
  }
373
403
  .dark {
374
- --roster-control-border: #44403c;
404
+ --roster-control-border: #a8a29e; /* gray-400 */
375
405
  --roster-control-bg: transparent;
376
406
  --roster-control-text: #f5f5f4;
377
- --roster-control-border-focus: #0f6498;
407
+ --roster-control-border-focus: #5ea3de; /* primary-400 */
378
408
  }
379
409
  ```
380
410
 
411
+ `--roster-control-border` is the one token here whose two scopes hold different
412
+ values, and that is deliberate. WCAG 1.4.11 asks 3:1 to identify a control, and
413
+ a border has to move opposite its surface to get there: gray-500 reaches 5.99
414
+ on white but only 2.53 on a gray-800 Dialog, while gray-400 reaches 6.01 there
415
+ and 2.52 on white. No single step of the ramp clears 3:1 both ways. The old
416
+ gray-300 / gray-700 pair was 1.49 and 1.48 — a hairline rather than a boundary.
417
+
381
418
  Set them in both scopes, for the same reason the scrollbar thumb needs both:
382
419
  Roster's own `.dark` rule has equal specificity and comes later.
383
420
 
384
- The defaults are the values the component used to hardcode, so installing this
385
- version changes nothing on its own. Placeholder color, the error state, and the
421
+ Three of the four still default to what the components used to hardcode.
422
+ `--roster-control-border` does not — the 1.4.11 pass moved it two ramp steps —
423
+ so installing this version **does** change how your fields look, deliberately. Placeholder color, the error state, and the
386
424
  focus ring are deliberately not on tokens — the ring is already
387
425
  `--roster-ring`, and error styling should stay recognizably an error.
388
426
 
389
427
  Not every control uses every token: `Select` takes the first three, because
390
- focus on its trigger is the shared `--roster-ring` rather than a border color.
428
+ focus on its trigger is the shared `--roster-ring` rather than a border color,
429
+ and it draws the border as a ring rather than a border.
391
430
  One family rather than one per component is deliberate — these controls sit in
392
431
  the same row of the same form and are drawn to look identical, so being able to
393
432
  repaint one and not the other would be a bug, not a choice.
394
433
 
395
- The `outline` variant is the only one that reads them, in every component, and
434
+ `outline` reads all four. `soft` reads the border only, because a boundary is
435
+ the one thing a filled field cannot supply for itself on an arbitrary surface;
436
+ `white`, `slate` and `ghost` stay opinionated. `Checkbox` reads the border too,
437
+ for its unchecked box. Historically `outline` was the only reader, and
396
438
  it carries no hover fill. The other variants each name a concrete surface
397
439
  (`white`, `soft`, `slate`, `ghost`) and stay opinionated: a token that meant
398
440
  something different in each would not be a token.
@@ -403,6 +445,56 @@ One caveat worth knowing before you lean on these: like `--roster-card-*`,
403
445
  inside `@layer base` is outranked, and an override scoped to a subtree rather
404
446
  than the document root will not reach them.
405
447
 
448
+ ### Floating surface colors
449
+
450
+ `Select`'s menu, `Dialog`'s `white` variant and `Tooltip`'s `themed` variant
451
+ read these, so one palette covers every panel that floats over the page.
452
+
453
+ ```css
454
+ :root {
455
+ --roster-popover-bg: #fff;
456
+ --roster-popover-border: #e7e5e4;
457
+ --roster-popover-text: #1c1917;
458
+ }
459
+ .dark {
460
+ --roster-popover-bg: #292524;
461
+ --roster-popover-border: #44403c;
462
+ --roster-popover-text: #f5f5f4;
463
+ }
464
+ ```
465
+
466
+ Separate from `--roster-control-*` on purpose, and the default is the reason:
467
+ `--roster-control-bg` is `transparent`, which is right for a field drawn on a
468
+ page and actively broken for a panel drawn over one. A control and the menu it
469
+ opens are also legitimately different surfaces in plenty of palettes.
470
+
471
+ **Set these at `:root`, not on a container.** The menu and the tooltip bubble
472
+ are portaled to `<body>`, so they are not descendants of whatever you rendered
473
+ the component inside: properties set on a wrapper reach the trigger and stop
474
+ there. Any ancestor of `<body>` works — `:root` is simply the one that always
475
+ is, which is also why the `.dark` block below reaches a menu whose `.dark`
476
+ scope `Select` copies across the portal. The result is a themed trigger opening an unthemed panel, which is the
477
+ exact problem this family exists to fix. Set them in both scopes too, for the
478
+ same reason the control family needs both.
479
+
480
+ Where each component takes them, and why not everywhere:
481
+
482
+ - **`Select`'s menu** has no variants, so it reads all three outright — panel
483
+ fill, hairline, and the option labels, which are `text-inherit` so the text
484
+ token actually reaches the only text in the panel.
485
+ - **`Dialog`** takes them in `white` only. `slate`, `primary` and `glass` each
486
+ name a specific surface, and a token that meant something different inside
487
+ each name would not be a token. The defaults are `white`'s previous values
488
+ exactly, in both schemes, so nothing moves.
489
+ - **`Tooltip`** has a `themed` variant that reads them. Its `dark` and `light`
490
+ variants are untouched: `dark` is the default, an inverted bubble that
491
+ deliberately reads the same on a light or a dark page, and pointing that at a
492
+ token whose light default is white would not theme it, it would delete it.
493
+
494
+ One appearance change ships with this: `Select`'s menu ring moves from
495
+ `black/5` to `--roster-gray-200` in light mode, which is the hairline the rest
496
+ of the library already draws.
497
+
406
498
  ### Reaching the control
407
499
 
408
500
  `className` on a field component lands on the outer `Field` wrapper. That is
@@ -411,12 +503,20 @@ cannot reach the element that draws the border, the height or the font.
411
503
 
412
504
  Every field therefore has a second prop, **named for the element it reaches**:
413
505
 
414
- | Component | Wrapper | The control itself |
415
- | -------------- | ----------- | -------------------- |
416
- | `Input` | `className` | `inputClassName` |
417
- | `PasswordInput`| `className` | `inputClassName` |
418
- | `Select` | `className` | `triggerClassName` |
419
- | `Textarea` | `className` | `textareaClassName` |
506
+ | Component | Wrapper | The control itself | Other inner parts |
507
+ | -------------- | ----------- | -------------------- | ------------------- |
508
+ | `Input` | `className` | `inputClassName` | — |
509
+ | `PasswordInput`| `className` | `inputClassName` | — |
510
+ | `Select` | `className` | `triggerClassName` | `optionsClassName` |
511
+ | `Textarea` | `className` | `textareaClassName` | — |
512
+
513
+ `Select` is the first component with two styleable inner parts, which is the
514
+ case the naming scheme was chosen for: `optionsClassName` reaches the popup
515
+ panel. One caveat that is a property of Headless UI rather than of this prop —
516
+ the panel's `max-height` and `overflow` are written inline by its `size`
517
+ middleware, so a height passed here lands in the class list and is outranked.
518
+ To cap the menu, set the variable that inline rule reads:
519
+ `optionsClassName="rst:[--anchor-max-height:20rem]"`.
420
520
 
421
521
  The names differ on purpose. A single `controlClassName` would name a concept
422
522
  rather than an element, and it breaks the first time a component has two
@@ -23,6 +23,22 @@ export interface SelectProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
23
23
  * is the escape hatch for the rest. Mirrors `Input`'s `inputClassName`.
24
24
  */
25
25
  triggerClassName?: string;
26
+ /**
27
+ * Classes for the popup panel.
28
+ *
29
+ * The escape-hatch rule already covers the wrapper (`className`) and the
30
+ * control (`triggerClassName`); the panel was the one styleable element with
31
+ * no way in. Mirrors `Input`'s `inputClassName`.
32
+ */
33
+ optionsClassName?: string;
34
+ /** Supporting text under the trigger. */
35
+ helperText?: string;
36
+ /**
37
+ * The message for an invalid selection. Implies `error`, so a caller cannot
38
+ * set the red ring and forget to say why — which is what `error` on its own
39
+ * did: a red outline and no text anywhere for anyone.
40
+ */
41
+ errorMessage?: string;
26
42
  }
27
- declare const Select: ({ options, value, onChange, placeholder, disabled, variant, size, error, className, triggerClassName, label, ...props }: SelectProps) => import("react/jsx-runtime").JSX.Element;
43
+ declare const Select: ({ options, value, onChange, placeholder, disabled, variant, size, error, className, triggerClassName, optionsClassName, helperText, errorMessage, label, ...props }: SelectProps) => import("react/jsx-runtime").JSX.Element;
28
44
  export { Select };
@@ -1,6 +1,14 @@
1
1
  import * as React from "react";
2
2
  export type TooltipPlacement = "top" | "bottom" | "left" | "right";
3
- export type TooltipVariant = "dark" | "light";
3
+ /**
4
+ * `dark` is an inverted bubble that deliberately reads the same on a light or
5
+ * a dark page, and `light` is its counterpart — both NAME a surface, so
6
+ * neither can be repointed at a consumer token without becoming something
7
+ * else. `themed` is the opt-in third: it reads `--roster-popover-*`, the same
8
+ * family Select's menu and Dialog's `white` variant read, so a host palette
9
+ * covers every floating surface at once.
10
+ */
11
+ export type TooltipVariant = "dark" | "light" | "themed";
4
12
  export interface TooltipProps {
5
13
  /** The tooltip's popup content. Accepts a string or any React node. */
6
14
  content: React.ReactNode;
package/dist/meta.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.8.0",
3
- "tests": 1100,
2
+ "version": "4.8.2",
3
+ "tests": 1132,
4
4
  "testFiles": 93
5
5
  }