@blakesteve/roster 4.7.0 → 4.8.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/README.md CHANGED
@@ -144,6 +144,14 @@ 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
+
147
155
  `Navbar`'s `themeMode` prop is a different thing: it describes what palette the
148
156
  bar paints _itself_ with. Pair them with `themeMode="auto"` and the nav follows
149
157
  whatever `ThemeToggle` sets.
@@ -349,6 +357,99 @@ browser honors `scrollbar-width` / `scrollbar-color`, and drops the WebKit
349
357
  pseudo-elements for any element that sets either of them, so on anything modern
350
358
  `thin` is the whole shape control available.
351
359
 
360
+ ### Control surface colors
361
+
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.
365
+
366
+ ```css
367
+ :root {
368
+ --roster-control-border: #d6d3d1;
369
+ --roster-control-bg: transparent;
370
+ --roster-control-text: #1c1917;
371
+ --roster-control-border-focus: #0f6498;
372
+ }
373
+ .dark {
374
+ --roster-control-border: #44403c;
375
+ --roster-control-bg: transparent;
376
+ --roster-control-text: #f5f5f4;
377
+ --roster-control-border-focus: #0f6498;
378
+ }
379
+ ```
380
+
381
+ Set them in both scopes, for the same reason the scrollbar thumb needs both:
382
+ Roster's own `.dark` rule has equal specificity and comes later.
383
+
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
386
+ focus ring are deliberately not on tokens — the ring is already
387
+ `--roster-ring`, and error styling should stay recognizably an error.
388
+
389
+ 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.
391
+ One family rather than one per component is deliberate — these controls sit in
392
+ the same row of the same form and are drawn to look identical, so being able to
393
+ repaint one and not the other would be a bug, not a choice.
394
+
395
+ The `outline` variant is the only one that reads them, in every component, and
396
+ it carries no hover fill. The other variants each name a concrete surface
397
+ (`white`, `soft`, `slate`, `ghost`) and stay opinionated: a token that meant
398
+ something different in each would not be a token.
399
+
400
+ One caveat worth knowing before you lean on these: like `--roster-card-*`,
401
+ `--roster-nav-*` and `--roster-footer-*`, they are declared outside
402
+ `@layer roster` and resolved at `:root`. A host that declares its override
403
+ inside `@layer base` is outranked, and an override scoped to a subtree rather
404
+ than the document root will not reach them.
405
+
406
+ ### Reaching the control
407
+
408
+ `className` on a field component lands on the outer `Field` wrapper. That is
409
+ right for layout — width, flex, margins — and useless for anything else: it
410
+ cannot reach the element that draws the border, the height or the font.
411
+
412
+ Every field therefore has a second prop, **named for the element it reaches**:
413
+
414
+ | Component | Wrapper | The control itself |
415
+ | -------------- | ----------- | -------------------- |
416
+ | `Input` | `className` | `inputClassName` |
417
+ | `PasswordInput`| `className` | `inputClassName` |
418
+ | `Select` | `className` | `triggerClassName` |
419
+ | `Textarea` | `className` | `textareaClassName` |
420
+
421
+ The names differ on purpose. A single `controlClassName` would name a concept
422
+ rather than an element, and it breaks the first time a component has two
423
+ styleable inner parts — a combobox has an input *and* a toggle. `triggerClassName`
424
+ also tells you something true: the thing you are styling is a `<button>`, so
425
+ `placeholder:` and `disabled:` behave differently on it than they would on an
426
+ `<input>`.
427
+
428
+ These merge last and win, not because they come later in the attribute (CSS
429
+ ignores that) but because `cn` is tailwind-merge and deletes the class they
430
+ conflict with.
431
+
432
+ ### Control heights
433
+
434
+ `Button`, `Input` and `Select` share one size scale, so they line up when set
435
+ side by side in a row:
436
+
437
+ | `size` | Height | `Button` padding | `Input` padding | `Select` padding |
438
+ | --------- | ------ | ---------------- | --------------- | ---------------- |
439
+ | `sm` | 36px | `px-3` | `px-3` | `pl-3 pr-9` |
440
+ | `default` | 40px | `px-4` | `px-4` | `pl-4 pr-10` |
441
+ | `lg` | 44px | `px-8` | `px-4` | `pl-4 pr-10` |
442
+
443
+ Padding diverges at `lg` on purpose. A button's label is centered and wants the
444
+ room, while a field's text is left-aligned and a wide inset only pushes the
445
+ caret toward the middle. `Select`'s right padding is clearance for the chevron,
446
+ which is a fixed 14px at every size, so it has nothing to grow for.
447
+
448
+ Unit tests assert the three components resolve to the same height at every
449
+ shared size, so they cannot drift apart. `Button` also has `xs` and `icon`,
450
+ which neither field has an equivalent for. `Textarea` is not on this scale — it
451
+ is multi-line, so the answer there is a min-height rather than a height.
452
+
352
453
  ### Components that render links
353
454
 
354
455
  `Breadcrumbs` renders a plain `<a>` by default, which is right for a static
@@ -408,7 +509,7 @@ function App() {
408
509
  | `Disclosure` | Show/hide toggle using HeadlessUI |
409
510
  | `Eyebrow` | Small tracked-out uppercase label above a heading or beside a rule; polymorphic via `as` |
410
511
  | `InlineCode` | Inline `<code>` for identifiers in running prose |
411
- | `Input` | Text input with label, error state, and icon slots |
512
+ | `Input` | Text input with label, error state, icon slots, and a size scale matching `Button` |
412
513
  | `LabeledDivider` | Horizontal rule carrying a label, with an optional trailing count |
413
514
  | `Link` | Styled anchor with variant support |
414
515
  | `PasswordInput` | Password field with a show/hide reveal toggle |
@@ -416,7 +517,7 @@ function App() {
416
517
  | `AvatarStrip` | Stacked avatar row with overflow chip, dismiss button, trailing slot, and label area |
417
518
  | `CollapsibleSection` | Clamps any content (prose, chips, image grids) to a fixed height with a fade and expand/collapse toggle |
418
519
  | `LiquidTabs` | Controlled tab strip with a liquid sliding pill indicator: pill and filled variants |
419
- | `Select` | Dropdown selector |
520
+ | `Select` | Dropdown selector, on the same size scale as `Button` and `Input` |
420
521
  | `SegmentBar` | Proportional horizontal bar divided into colored segments with optional legend |
421
522
  | `Stat` | A single figure with its label and, optionally, where the figure came from |
422
523
  | `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,15 @@ 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;
16
26
  }
17
- declare const Select: ({ options, value, onChange, placeholder, disabled, variant, error, className, label, }: SelectProps) => import("react/jsx-runtime").JSX.Element;
27
+ declare const Select: ({ options, value, onChange, placeholder, disabled, variant, size, error, className, triggerClassName, label, ...props }: SelectProps) => import("react/jsx-runtime").JSX.Element;
18
28
  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 };
package/dist/meta.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.7.0",
3
- "tests": 1039,
2
+ "version": "4.8.0",
3
+ "tests": 1100,
4
4
  "testFiles": 93
5
5
  }