@blakesteve/roster 4.7.0 → 4.8.1

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
@@ -144,6 +144,20 @@ document head:
144
144
  If you pass a custom `storageKey`, change the script to match — a mismatch means
145
145
  the script and the toggle disagree, which shows up as a flash on every reload.
146
146
 
147
+ **Components that render into a portal**, such as `Select`'s menu, are attached
148
+ to `<body>` rather than to the element you wrote them next to. When `.dark` is
149
+ on `<html>` — what `ThemeToggle` does, and what the script above sets up — that
150
+ makes no difference, because the portal is still a descendant. If you scope
151
+ `.dark` to a subtree instead, `Select` detects the nearest `.dark` ancestor of
152
+ the field and carries it across the portal, so the menu follows the same scope
153
+ the trigger is in.
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
+
147
161
  `Navbar`'s `themeMode` prop is a different thing: it describes what palette the
148
162
  bar paints _itself_ with. Pair them with `themeMode="auto"` and the nav follows
149
163
  whatever `ThemeToggle` sets.
@@ -349,6 +363,157 @@ browser honors `scrollbar-width` / `scrollbar-color`, and drops the WebKit
349
363
  pseudo-elements for any element that sets either of them, so on anything modern
350
364
  `thin` is the whole shape control available.
351
365
 
366
+ ### Control surface colors
367
+
368
+ `Input`, `PasswordInput` and `Select` read these variables in their `outline`
369
+ variant, so a consumer can repaint a form control without restating the class
370
+ list.
371
+
372
+ ```css
373
+ :root {
374
+ --roster-control-border: #d6d3d1;
375
+ --roster-control-bg: transparent;
376
+ --roster-control-text: #1c1917;
377
+ --roster-control-border-focus: #0f6498;
378
+ }
379
+ .dark {
380
+ --roster-control-border: #44403c;
381
+ --roster-control-bg: transparent;
382
+ --roster-control-text: #f5f5f4;
383
+ --roster-control-border-focus: #0f6498;
384
+ }
385
+ ```
386
+
387
+ Set them in both scopes, for the same reason the scrollbar thumb needs both:
388
+ Roster's own `.dark` rule has equal specificity and comes later.
389
+
390
+ The defaults are the values the component used to hardcode, so installing this
391
+ version changes nothing on its own. Placeholder color, the error state, and the
392
+ focus ring are deliberately not on tokens — the ring is already
393
+ `--roster-ring`, and error styling should stay recognizably an error.
394
+
395
+ Not every control uses every token: `Select` takes the first three, because
396
+ focus on its trigger is the shared `--roster-ring` rather than a border color.
397
+ One family rather than one per component is deliberate — these controls sit in
398
+ the same row of the same form and are drawn to look identical, so being able to
399
+ repaint one and not the other would be a bug, not a choice.
400
+
401
+ The `outline` variant is the only one that reads them, in every component, and
402
+ it carries no hover fill. The other variants each name a concrete surface
403
+ (`white`, `soft`, `slate`, `ghost`) and stay opinionated: a token that meant
404
+ something different in each would not be a token.
405
+
406
+ One caveat worth knowing before you lean on these: like `--roster-card-*`,
407
+ `--roster-nav-*` and `--roster-footer-*`, they are declared outside
408
+ `@layer roster` and resolved at `:root`. A host that declares its override
409
+ inside `@layer base` is outranked, and an override scoped to a subtree rather
410
+ than the document root will not reach them.
411
+
412
+ ### Floating surface colors
413
+
414
+ `Select`'s menu, `Dialog`'s `white` variant and `Tooltip`'s `themed` variant
415
+ read these, so one palette covers every panel that floats over the page.
416
+
417
+ ```css
418
+ :root {
419
+ --roster-popover-bg: #fff;
420
+ --roster-popover-border: #e7e5e4;
421
+ --roster-popover-text: #1c1917;
422
+ }
423
+ .dark {
424
+ --roster-popover-bg: #292524;
425
+ --roster-popover-border: #44403c;
426
+ --roster-popover-text: #f5f5f4;
427
+ }
428
+ ```
429
+
430
+ Separate from `--roster-control-*` on purpose, and the default is the reason:
431
+ `--roster-control-bg` is `transparent`, which is right for a field drawn on a
432
+ page and actively broken for a panel drawn over one. A control and the menu it
433
+ opens are also legitimately different surfaces in plenty of palettes.
434
+
435
+ **Set these at `:root`, not on a container.** The menu and the tooltip bubble
436
+ are portaled to `<body>`, so they are not descendants of whatever you rendered
437
+ the component inside: properties set on a wrapper reach the trigger and stop
438
+ there. Any ancestor of `<body>` works — `:root` is simply the one that always
439
+ is, which is also why the `.dark` block below reaches a menu whose `.dark`
440
+ scope `Select` copies across the portal. The result is a themed trigger opening an unthemed panel, which is the
441
+ exact problem this family exists to fix. Set them in both scopes too, for the
442
+ same reason the control family needs both.
443
+
444
+ Where each component takes them, and why not everywhere:
445
+
446
+ - **`Select`'s menu** has no variants, so it reads all three outright — panel
447
+ fill, hairline, and the option labels, which are `text-inherit` so the text
448
+ token actually reaches the only text in the panel.
449
+ - **`Dialog`** takes them in `white` only. `slate`, `primary` and `glass` each
450
+ name a specific surface, and a token that meant something different inside
451
+ each name would not be a token. The defaults are `white`'s previous values
452
+ exactly, in both schemes, so nothing moves.
453
+ - **`Tooltip`** has a `themed` variant that reads them. Its `dark` and `light`
454
+ variants are untouched: `dark` is the default, an inverted bubble that
455
+ deliberately reads the same on a light or a dark page, and pointing that at a
456
+ token whose light default is white would not theme it, it would delete it.
457
+
458
+ One appearance change ships with this: `Select`'s menu ring moves from
459
+ `black/5` to `--roster-gray-200` in light mode, which is the hairline the rest
460
+ of the library already draws.
461
+
462
+ ### Reaching the control
463
+
464
+ `className` on a field component lands on the outer `Field` wrapper. That is
465
+ right for layout — width, flex, margins — and useless for anything else: it
466
+ cannot reach the element that draws the border, the height or the font.
467
+
468
+ Every field therefore has a second prop, **named for the element it reaches**:
469
+
470
+ | Component | Wrapper | The control itself | Other inner parts |
471
+ | -------------- | ----------- | -------------------- | ------------------- |
472
+ | `Input` | `className` | `inputClassName` | — |
473
+ | `PasswordInput`| `className` | `inputClassName` | — |
474
+ | `Select` | `className` | `triggerClassName` | `optionsClassName` |
475
+ | `Textarea` | `className` | `textareaClassName` | — |
476
+
477
+ `Select` is the first component with two styleable inner parts, which is the
478
+ case the naming scheme was chosen for: `optionsClassName` reaches the popup
479
+ panel. One caveat that is a property of Headless UI rather than of this prop —
480
+ the panel's `max-height` and `overflow` are written inline by its `size`
481
+ middleware, so a height passed here lands in the class list and is outranked.
482
+ To cap the menu, set the variable that inline rule reads:
483
+ `optionsClassName="rst:[--anchor-max-height:20rem]"`.
484
+
485
+ The names differ on purpose. A single `controlClassName` would name a concept
486
+ rather than an element, and it breaks the first time a component has two
487
+ styleable inner parts — a combobox has an input *and* a toggle. `triggerClassName`
488
+ also tells you something true: the thing you are styling is a `<button>`, so
489
+ `placeholder:` and `disabled:` behave differently on it than they would on an
490
+ `<input>`.
491
+
492
+ These merge last and win, not because they come later in the attribute (CSS
493
+ ignores that) but because `cn` is tailwind-merge and deletes the class they
494
+ conflict with.
495
+
496
+ ### Control heights
497
+
498
+ `Button`, `Input` and `Select` share one size scale, so they line up when set
499
+ side by side in a row:
500
+
501
+ | `size` | Height | `Button` padding | `Input` padding | `Select` padding |
502
+ | --------- | ------ | ---------------- | --------------- | ---------------- |
503
+ | `sm` | 36px | `px-3` | `px-3` | `pl-3 pr-9` |
504
+ | `default` | 40px | `px-4` | `px-4` | `pl-4 pr-10` |
505
+ | `lg` | 44px | `px-8` | `px-4` | `pl-4 pr-10` |
506
+
507
+ Padding diverges at `lg` on purpose. A button's label is centered and wants the
508
+ room, while a field's text is left-aligned and a wide inset only pushes the
509
+ caret toward the middle. `Select`'s right padding is clearance for the chevron,
510
+ which is a fixed 14px at every size, so it has nothing to grow for.
511
+
512
+ Unit tests assert the three components resolve to the same height at every
513
+ shared size, so they cannot drift apart. `Button` also has `xs` and `icon`,
514
+ which neither field has an equivalent for. `Textarea` is not on this scale — it
515
+ is multi-line, so the answer there is a min-height rather than a height.
516
+
352
517
  ### Components that render links
353
518
 
354
519
  `Breadcrumbs` renders a plain `<a>` by default, which is right for a static
@@ -408,7 +573,7 @@ function App() {
408
573
  | `Disclosure` | Show/hide toggle using HeadlessUI |
409
574
  | `Eyebrow` | Small tracked-out uppercase label above a heading or beside a rule; polymorphic via `as` |
410
575
  | `InlineCode` | Inline `<code>` for identifiers in running prose |
411
- | `Input` | Text input with label, error state, and icon slots |
576
+ | `Input` | Text input with label, error state, icon slots, and a size scale matching `Button` |
412
577
  | `LabeledDivider` | Horizontal rule carrying a label, with an optional trailing count |
413
578
  | `Link` | Styled anchor with variant support |
414
579
  | `PasswordInput` | Password field with a show/hide reveal toggle |
@@ -416,7 +581,7 @@ function App() {
416
581
  | `AvatarStrip` | Stacked avatar row with overflow chip, dismiss button, trailing slot, and label area |
417
582
  | `CollapsibleSection` | Clamps any content (prose, chips, image grids) to a fixed height with a fade and expand/collapse toggle |
418
583
  | `LiquidTabs` | Controlled tab strip with a liquid sliding pill indicator: pill and filled variants |
419
- | `Select` | Dropdown selector |
584
+ | `Select` | Dropdown selector, on the same size scale as `Button` and `Input` |
420
585
  | `SegmentBar` | Proportional horizontal bar divided into colored segments with optional legend |
421
586
  | `Stat` | A single figure with its label and, optionally, where the figure came from |
422
587
  | `Spinner` | Loading indicator |
@@ -1,12 +1,21 @@
1
1
  import { default as React } from 'react';
2
2
  import { VariantProps } from 'class-variance-authority';
3
3
  import { inputVariants } from './input-variants';
4
- export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>, VariantProps<typeof inputVariants> {
4
+ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">, VariantProps<typeof inputVariants> {
5
5
  label?: string;
6
6
  helperText?: string;
7
7
  errorMessage?: string;
8
8
  startIcon?: React.ReactNode;
9
9
  endIcon?: React.ReactNode;
10
+ /**
11
+ * Classes for the `<input>` itself.
12
+ *
13
+ * `className` lands on the outer `Field` wrapper, which is right for layout
14
+ * and useless for anything else: a consumer could not reach the control to
15
+ * change its height, border or background. `size` covers the common case;
16
+ * this is the escape hatch for the rest.
17
+ */
18
+ inputClassName?: string;
10
19
  }
11
20
  declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
12
21
  export { Input };
@@ -1,5 +1,6 @@
1
1
  export declare const inputVariants: (props?: ({
2
2
  variant?: "soft" | "outline" | "white" | "ghost" | "slate" | null | undefined;
3
+ size?: "sm" | "lg" | "default" | null | undefined;
3
4
  error?: boolean | null | undefined;
4
5
  } & import('class-variance-authority/types').ClassProp) | undefined) => string;
5
6
  export declare const iconVariants: (props?: ({
@@ -1,3 +1,4 @@
1
+ import { default as React } from 'react';
1
2
  import { VariantProps } from 'class-variance-authority';
2
3
  import { selectTriggerVariants } from './select-variants';
3
4
  export type SelectOption = {
@@ -13,6 +14,31 @@ export interface SelectProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
13
14
  disabled?: boolean;
14
15
  className?: string;
15
16
  label?: string;
17
+ /**
18
+ * Classes for the trigger button itself.
19
+ *
20
+ * `className` lands on the outer `Field` wrapper, which is right for layout
21
+ * and useless for anything else: a consumer could not reach the control to
22
+ * change its height, ring or background. `size` covers the common case; this
23
+ * is the escape hatch for the rest. Mirrors `Input`'s `inputClassName`.
24
+ */
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;
16
42
  }
17
- declare const Select: ({ options, value, onChange, placeholder, disabled, variant, error, className, label, }: 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;
18
44
  export { Select };
@@ -1,4 +1,8 @@
1
1
  export declare const selectTriggerVariants: (props?: ({
2
2
  variant?: "soft" | "outline" | "white" | "ghost" | "slate" | null | undefined;
3
+ size?: "sm" | "lg" | "default" | null | undefined;
3
4
  error?: boolean | null | undefined;
4
5
  } & import('class-variance-authority/types').ClassProp) | undefined) => string;
6
+ export declare const selectOptionVariants: (props?: ({
7
+ size?: "sm" | "lg" | "default" | null | undefined;
8
+ } & import('class-variance-authority/types').ClassProp) | undefined) => string;
@@ -5,6 +5,15 @@ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextArea
5
5
  label?: string;
6
6
  helperText?: string;
7
7
  errorMessage?: string;
8
+ /**
9
+ * Classes for the `<textarea>` itself.
10
+ *
11
+ * `className` lands on the outer `Field` wrapper, so `<Textarea
12
+ * className="rst:h-40" />` has always sized the wrapper and left the control
13
+ * alone. Mirrors `Input`'s `inputClassName` and `Select`'s
14
+ * `triggerClassName`: the hatch is named for the element it reaches.
15
+ */
16
+ textareaClassName?: string;
8
17
  }
9
18
  declare const Textarea: React.ForwardRefExoticComponent<TextareaProps & React.RefAttributes<HTMLTextAreaElement>>;
10
19
  export { Textarea };
@@ -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.7.0",
3
- "tests": 1039,
2
+ "version": "4.8.1",
3
+ "tests": 1117,
4
4
  "testFiles": 93
5
5
  }