@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 +167 -2
- package/dist/components/atoms/Input/Input.d.ts +10 -1
- package/dist/components/atoms/Input/input-variants.d.ts +1 -0
- package/dist/components/atoms/Select/Select.d.ts +27 -1
- package/dist/components/atoms/Select/select-variants.d.ts +4 -0
- package/dist/components/atoms/Textarea/Textarea.d.ts +9 -0
- package/dist/components/atoms/Tooltip/Tooltip.d.ts +9 -1
- package/dist/meta.json +2 -2
- package/dist/roster.cjs.js +2 -2
- package/dist/roster.css +1 -1
- package/dist/roster.es.js +2025 -1813
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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