@blakesteve/roster 5.17.0 → 5.19.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
@@ -186,6 +186,72 @@ Tokens Roster never declares, such as the [shape tokens](#shape) and
186
186
  `--roster-elevation-control`, have no such rule to lose to, so `:root` alone
187
187
  covers both schemes.
188
188
 
189
+ #### A region with its own palette
190
+
191
+ Roster's semantic tokens, such as `--roster-tabs-indicator` and
192
+ `--roster-card-bg`, derive from the ramps at `:root`. A custom property resolves
193
+ where it's declared, so they reach the page as finished values, and a ramp
194
+ overridden on a wrapper changes nothing below it: inside that wrapper, tab
195
+ underlines and timeline rails keep the page's color. Add `data-roster-scope` and
196
+ they derive again from the ramps in force on that element:
197
+
198
+ ```css
199
+ .region {
200
+ --roster-primary-400: #a78bfa;
201
+ --roster-primary-500: #7c3aed;
202
+ --roster-primary-600: #6d28d9;
203
+ }
204
+ ```
205
+
206
+ ```tsx
207
+ <section className="region" data-roster-scope>
208
+ <Tabs tabs={tabs} activeTab={active} onChange={setActive} />
209
+ </section>
210
+ ```
211
+
212
+ A page that never sets the attribute computes exactly what it did before.
213
+
214
+ **A scope owns its state.** The attribute matches the light tokens directly, so
215
+ a scope inside a dark page, with no `.dark` of its own, resets to light while
216
+ everything around it stays dark. To stay dark, a scope sets `.dark` too: an
217
+ element with both gets the dark values, from its own ramps.
218
+
219
+ ```tsx
220
+ <section className="region dark" data-roster-scope>
221
+ ```
222
+
223
+ A scope that follows the page's theme has to do that itself, at two costs:
224
+
225
+ - `ThemeToggle` and the blocking script above reach `<html>` and nothing else.
226
+ Until your code sets `.dark` on the scope, a server-rendered scope paints
227
+ light on a dark page.
228
+ - `Select`, `Combobox`, `MultiSelect`, `Sheet` and the `Toaster` read a scoped
229
+ `.dark` once, when they mount. Give the scope a `key` that changes with the
230
+ theme, so they mount again and their panels follow it.
231
+
232
+ A few more things follow from the same rule:
233
+
234
+ - Scopes nest. An inner scope derives from the ramps in force at the inner
235
+ element, its own if it sets any and the outer scope's if it doesn't.
236
+ - A semantic token you set at `:root` stops at a scope, which declares its own.
237
+ Set it on the scope as well. `[data-roster-scope]` weighs the same as a class
238
+ and sits outside any layer, so a class on the scope has to load after Roster's
239
+ stylesheet, unlayered, or it's discarded, as in the rule above.
240
+ - Sheet timing and elevation read no ramp, so a scope doesn't re-declare them.
241
+ It inherits them, so a light scope in a dark page keeps the dark shadows and
242
+ the dark Sheet backdrop.
243
+
244
+ Two things it doesn't do:
245
+
246
+ - **It doesn't cross a portal.** `Select`'s menu, `Menu`, `Dialog` and `Tooltip`
247
+ render at `<body>`, outside the scope, so the scope's ramps don't reach them.
248
+ A `.dark` on the scope is carried across by the five surfaces above and by
249
+ `Menu`, which then open on Roster's own dark tokens rather than your page's.
250
+ `Dialog` and `Tooltip` don't carry it, so they open in the page's scheme.
251
+ - **It doesn't stop `dark:` variants.** They fire under any `.dark` ancestor,
252
+ and a scope can't undo one, so a light scope in a dark page gets light tokens
253
+ and dark variants together.
254
+
189
255
  ### Elevation
190
256
 
191
257
  Three levels, each a themeable box-shadow, and the only way a surface should
@@ -754,8 +820,9 @@ something different in each would not be a token.
754
820
  One caveat worth knowing before you lean on these: like `--roster-card-*`,
755
821
  `--roster-nav-*` and `--roster-footer-*`, they are declared outside
756
822
  `@layer roster` and resolved at `:root`. A host that declares its override
757
- inside `@layer base` is outranked, and an override scoped to a subtree rather
758
- than the document root will not reach them.
823
+ inside `@layer base` is outranked, and a ramp overridden on a subtree rather
824
+ than the document root will not reach them unless that subtree carries
825
+ [`data-roster-scope`](#a-region-with-its-own-palette).
759
826
 
760
827
  ### Floating surface colors
761
828
 
@@ -1328,6 +1395,12 @@ rather than a rewrite.
1328
1395
  short scannable set that would otherwise spend three rows of a wide form. It
1329
1396
  sets `aria-orientation` to match.
1330
1397
 
1398
+ `disabled` dims the whole group: its label, every option and the helper text.
1399
+ The dimmed text measures under 4.5:1, and a contrast checker such as axe will
1400
+ say so. That's on purpose: WCAG 1.4.3 exempts text that's part of an inactive
1401
+ control, and a disabled group is one. The same goes for `CheckboxGroup` and
1402
+ `RadioTileGroup`.
1403
+
1331
1404
  Every radio is a 44x44 target, and the component is built so that it stays one:
1332
1405
  each option's box is sized to contain its own target, which is why the options
1333
1406
  can sit close together without a target ever reaching into its neighbor.
@@ -1340,6 +1413,68 @@ escape the box. Lay the options out across with `orientation` rather than
1340
1413
  through `optionsClassName`, for the same reason: the horizontal spacing that
1341
1414
  keeps two radios 44px apart rides on that prop.
1342
1415
 
1416
+ ### One answer, chosen by its picture
1417
+
1418
+ `RadioTileGroup` is `RadioGroup` with tiles for options: a picture, a name and
1419
+ an optional description each, and no radio dot. For choosing between photos,
1420
+ avatars, swatches or layouts, where people know the options by sight. It's
1421
+ the same radio group to a keyboard and a screen reader: one tab stop, the
1422
+ arrows move and choose (wrapping, and skipping a disabled tile), Space
1423
+ chooses, and Home and End go to the first and last tile. `value`, `onChange`,
1424
+ `label`, `helperText`, `errorMessage` and `disabled` work as in `RadioGroup`,
1425
+ and `required` sets `aria-required`.
1426
+
1427
+ ```tsx
1428
+ <RadioTileGroup
1429
+ label="Cover photo"
1430
+ value={cover}
1431
+ onChange={setCover}
1432
+ options={[
1433
+ { value: "harbor", label: "Harbor at dawn", picture: <img src={harbor} alt="" /> },
1434
+ { value: "ridge", label: "Pine ridge", description: "Fog in the valley", picture: <img src={ridge} alt="" /> },
1435
+ { value: "flats", label: "Salt flats", picture: <img src={flats} alt="" /> },
1436
+ ]}
1437
+ />
1438
+ ```
1439
+
1440
+ A tile is named by its `label` alone. The picture is hidden from assistive
1441
+ technology so the name isn't read twice, which is why the images above have
1442
+ `alt=""`.
1443
+
1444
+ The arrows follow the tiles' order, not the grid's rows: Down is the next
1445
+ tile, as Right is. A row is however many tiles fit the width, so moving by
1446
+ row would change with the window, and the radio pattern a screen reader
1447
+ announces says Down is next.
1448
+
1449
+ **Laying it out.** Two tiles to a row, then three from the `sm` breakpoint.
1450
+ `columns` takes one count, or a count per breakpoint, where one left out takes
1451
+ the count below it: `columns={{ base: 2, md: 4 }}`. `minTileWidth="9rem"`
1452
+ instead fits as many tiles to a row as the group's own width holds, whatever
1453
+ the viewport. In the default `outlined` look an `<img>` placed directly in the
1454
+ picture is cropped square, so photos of any shape make even rows; anything
1455
+ else keeps its own shape, and `pictureClassName` changes either.
1456
+
1457
+ **Drawing your own chosen look.** Each tile, the element with `role="radio"`,
1458
+ carries `data-checked`, `data-disabled`, `data-hover` and `data-focus-visible`,
1459
+ and takes `tileClassName`. `variant="plain"` drops Roster's edge, fill and
1460
+ padding, so there's nothing to undo before drawing a frame or a stamp of your
1461
+ own. Roster's styles sit in a cascade layer below yours, so plain CSS wins
1462
+ without `!important`:
1463
+
1464
+ ```css
1465
+ .cover [data-checked] .frame { box-shadow: inset 0 0 0 3px #1c1917; }
1466
+ .cover [data-checked] .stamp { opacity: 1; }
1467
+ ```
1468
+
1469
+ The focus ring stays Roster's in both looks, offset outside the tile, so
1470
+ it's outside anything drawn on the tile or inside it.
1471
+
1472
+ **Tokens.** The `outlined` look reads `--roster-tile-border` and
1473
+ `--roster-tile-bg` at rest, and `--roster-tile-border-checked` and
1474
+ `--roster-tile-bg-checked` on the chosen tile, whose edge is drawn two pixels
1475
+ wide so it differs in weight as well as color. Light and dark values ship in
1476
+ `roster.css`.
1477
+
1343
1478
  ### Dialog
1344
1479
 
1345
1480
  A modal dialog. Focus moves in when it opens, Tab stays inside, Escape and its
@@ -1732,7 +1867,8 @@ label and value on one baseline above the bar, an optional detail line below.
1732
1867
  numbered marker and connectors that fill behind the current one;
1733
1868
  `"segmented"` is the compact form, always with "Step 2 of 5". The current
1734
1869
  step is `aria-current="step"`, finished ones say "done", and the position is
1735
- said when it changes.
1870
+ said when it changes. For steps down the page with content of their own, see
1871
+ [Timeline](#timeline).
1736
1872
 
1737
1873
  **LoadingDots.** Only for a model generating before its first token. It takes
1738
1874
  the surrounding text's size and color, rests at 1, 0.6 and 0.3 opacity (also
@@ -1741,6 +1877,56 @@ shown beside it.
1741
1877
 
1742
1878
  Every string in all five is a prop with an English default.
1743
1879
 
1880
+ ### Timeline
1881
+
1882
+ Steps down the page, each with content of its own: a schedule, a history, an
1883
+ onboarding list. Every step has a `title`, any `content` under it, a leading
1884
+ `when` column, a trailing `actions` column, and a marker on a line that runs
1885
+ through them all: its number, a check once done, `"hollow"` for an empty step,
1886
+ or a mark of your own.
1887
+
1888
+ ```tsx
1889
+ <Timeline
1890
+ label="The course, week by week"
1891
+ current={3}
1892
+ nowBefore={4}
1893
+ labels={{ now: "Today" }}
1894
+ steps={[
1895
+ { id: "w1", when: "Week 1 · Sep 2", title: "Soil and compost", actions: <Button size="sm">Edit</Button> },
1896
+ { id: "w2", when: "Week 2 · Sep 9", title: "Sowing in rows", content: "Depth, spacing and labels." },
1897
+ { id: "w3", when: "Week 3 · Sep 16", title: "Water and mulch" },
1898
+ { id: "w4", when: "Week 4 · Sep 23", title: "No topic yet", marker: "hollow" },
1899
+ ]}
1900
+ />
1901
+ ```
1902
+
1903
+ `current` is the step you're on, from 1: the steps before it are done, and
1904
+ past the last means every step is. `nowBefore` draws a "now" divider just
1905
+ before a step, where today falls, which needn't be beside the current step.
1906
+
1907
+ Every column comes from one grid, so the columns line up from step to step
1908
+ whatever each step holds, and a column no step uses takes no room. Below 40rem
1909
+ of its own width (not the window's), the `when` moves above the body and the
1910
+ actions under it.
1911
+
1912
+ It's an ordered list with a name (`label`, or `aria-labelledby` for a heading
1913
+ on the page). The current step is `aria-current="step"`, and a finished one
1914
+ says "done" after its title. The markers, the line and the divider are
1915
+ decoration, hidden from assistive technology. It takes no keys: a step's
1916
+ buttons and links are reached in reading order, which is the order they show.
1917
+
1918
+ Each `<li>` carries `data-step` (`done`, `current` or `upcoming`), and every
1919
+ part takes a class of its own. A mark of your own needs a background, or the
1920
+ line shows through it. The line reads `--roster-timeline-rail` ahead and
1921
+ `--roster-timeline-rail-done` behind the current step, and the divider
1922
+ `--roster-timeline-now`. In forced colors they become GrayText, Highlight and
1923
+ CanvasText.
1924
+
1925
+ **Why not `StepProgress orientation="vertical"`.** A timeline isn't always
1926
+ progress: a schedule doesn't advance while you watch it, so there's no "Step 3
1927
+ of 5" status here, and each step carries content where `StepProgress` takes
1928
+ names. The two share their markers.
1929
+
1744
1930
  ### Menu
1745
1931
 
1746
1932
  A button that opens a panel of actions: the menu button pattern. Compose it:
@@ -2431,8 +2617,10 @@ function App() {
2431
2617
  | `SkeletonCard`, `SkeletonTableRow`, `SkeletonAvatar`, `SkeletonStat` | Skeleton presets in the real component's box |
2432
2618
  | `ProgressField` | Progress with a label and value on one baseline and a detail line: the form to reach for |
2433
2619
  | `StepProgress` | Named steps: a stepper with markers and connectors, or a compact segmented "Step x of y" |
2620
+ | `Timeline` | Steps down the page with content of their own: a when column, a marker on a line, the body and actions in one grid; a "now" divider (see [Timeline](#timeline)) |
2434
2621
  | `Pullquote` | A line lifted out of prose, as `<figure>` + `<blockquote>` + `<figcaption>` |
2435
2622
  | `RadioGroup` | Radio set reporting one `string`; descriptions, error, 44px targets |
2623
+ | `RadioTileGroup` | A radio group of picture tiles: one tab stop, the arrows, Home and End; `data-checked` for a chosen look of your own (see [One answer, chosen by its picture](#one-answer-chosen-by-its-picture)) |
2436
2624
  | `Slider` | A value along a track, starting in the middle or not set on request; named snap-point chips, a visible number, the full keyboard, 44px targets (see [Slider](#slider)) |
2437
2625
  | `RangeSlider` | A low and a high on one track: two thumbs that meet and never cross, each its own slider to the keyboard (see [RangeSlider](#rangeslider)) |
2438
2626
  | `Tabs` | Tab strip, plain (an underline) or liquid (a sliding pill); arrow keys, one tab stop, counts, 44px `lg` size (see [Tabs](#tabs)) |
@@ -2673,7 +2861,7 @@ Output in `dist/`:
2673
2861
  | `tokens.css` | Design token CSS variables |
2674
2862
  | `preflight.css` | Optional global reset (see Setup) |
2675
2863
  | `index.d.ts` | TypeScript definitions |
2676
- | `meta.json` | Test count and version, written when the tarball is built |
2864
+ | `meta.json` | Test count, version and passes on retry, written when the tarball is built |
2677
2865
 
2678
2866
  ### What `meta.json` is for
2679
2867
 
@@ -2707,6 +2895,19 @@ rather than failing, and "live · 0 tests" would look authoritative while being
2707
2895
  the worst possible answer. `src/write-meta.test.ts` pins that guard and the
2708
2896
  field selection, including the one that bit first: `numTotalTestSuites` reads
2709
2897
  like a file count but counts `describe` blocks — 166 against 92 real files.
2898
+ `testFiles` counts each test file that ran, once: vitest's report has an entry
2899
+ for every file in every project and browser that collects it, skipped or not:
2900
+ 699 entries for 219 files on 2 Nov 2026.
2901
+
2902
+ It never writes from a failed run, and vitest's exit code decides what failed.
2903
+ Each failing test gets one retry (`vitest run --retry=1`): a real-browser test
2904
+ can time out once on a busy machine and pass on its second try, and a publish
2905
+ shouldn't stop for that. Anything else that fails the run stops it: a test that
2906
+ fails twice, a file that throws as it loads, an error thrown after its test has
2907
+ passed. Then `prepack` exits non-zero, what failed is printed (vitest's own
2908
+ output is hidden there), and no `meta.json` is written. A test that passed only
2909
+ on its retry is named in the output too, and `meta.json` records how many there
2910
+ were, as `passedOnRetry`.
2710
2911
 
2711
2912
  ## Contributing
2712
2913
 
@@ -0,0 +1 @@
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("react/jsx-runtime"),j=require("react"),b=require("../../../node_modules/@headlessui/react/dist/components/description/description.cjs"),S=require("../../../node_modules/@headlessui/react/dist/components/field/field.cjs"),h=require("../../../node_modules/@headlessui/react/dist/components/label/label.cjs"),g=require("../../../node_modules/@headlessui/react/dist/components/radio-group/radio-group.cjs"),s=require("../../../lib/utils.cjs"),k=require("../../../internal/field-invalid.cjs"),z=require("../../../internal/key-focus.cjs"),m=require("./radio-tile-group-variants.cjs"),A=j.forwardRef(function(r,i){return t.jsx("div",{ref:i,...r,"data-focus-visible":r["data-focus"]})});function B(a){const r=typeof a=="number"?{base:a}:a,i=r.base??1,l=r.sm??i,o=r.md??l,n=r.lg??o;return{"--rst-tiles-cols":i,"--rst-tiles-cols-sm":l,"--rst-tiles-cols-md":o,"--rst-tiles-cols-lg":n}}const C=({options:a,value:r,onChange:i,label:l,helperText:o,errorMessage:n,disabled:u=!1,required:q=!1,variant:w="outlined",columns:N={base:2,sm:3},minTileWidth:d,className:R,optionsClassName:G,tileClassName:T,pictureClassName:D,labelClassName:P,optionLabelClassName:V,descriptionClassName:F,messageClassName:I,onKeyDown:K,"aria-invalid":E,...v})=>{z.useKeyFocus();const p=!!n,H=k.isFieldInvalid(p,{...v,"aria-invalid":E}),L=k.withoutInvalidProps(v),y=w??"outlined",f=j.useRef(new Map),x=u?[]:a.filter(e=>!e.disabled),O=e=>{if(K?.(e),e.defaultPrevented||e.key!=="Home"&&e.key!=="End")return;const c=e.key==="Home"?x[0]:x[x.length-1];c&&(e.preventDefault(),f.current.get(c.value)?.focus(),c.value!==r&&i(c.value))};return t.jsxs(g.RadioGroup,{value:r,onChange:i,disabled:u,className:s.cn("rst:font-ui rst:flex rst:flex-col rst:gap-1.5 rst:min-w-0",R),...L,onKeyDown:O,"aria-invalid":H||void 0,"aria-required":q||void 0,children:[l&&t.jsx(h.Label,{className:s.cn("rst:block rst:text-sm rst:font-medium rst:text-[var(--roster-control-text)] rst:text-left",u&&"rst:opacity-50",P),children:l}),t.jsx("div",{className:s.cn(m.radioTileGroupOptionsVariants({fit:d?"width":"columns"}),G),style:d?{"--rst-tiles-min":d}:B(N),children:a.map(e=>t.jsx(S.Field,{disabled:u||e.disabled,className:"rst:flex rst:min-w-0",children:t.jsxs(g.Radio,{as:A,value:e.value,ref:c=>{c?f.current.set(e.value,c):f.current.delete(e.value)},className:s.cn(m.radioTileVariants({variant:y}),T),children:[e.picture!=null&&t.jsx("span",{"aria-hidden":"true",className:s.cn(m.radioTilePictureVariants({variant:y}),D),children:e.picture}),t.jsx(h.Label,{as:"span",passive:!0,className:s.cn("rst:block rst:text-sm rst:font-medium rst:text-[var(--roster-control-text)] rst:[overflow-wrap:anywhere]","rst:forced-colors:group-data-checked:underline rst:forced-colors:group-data-checked:decoration-2 rst:forced-colors:group-data-checked:underline-offset-4",V),children:e.label}),e.description&&t.jsx(b.Description,{as:"span",className:s.cn("rst:block rst:text-xs rst:text-gray-500 rst:dark:text-gray-400",F),children:e.description})]})},e.value))}),(o||n)&&t.jsx(b.Description,{as:"p",className:s.cn("rst:text-xs rst:text-left",p?"rst:text-error-600 rst:dark:text-error-400 rst:font-medium":"rst:text-gray-500 rst:dark:text-gray-400",u&&"rst:opacity-50",I),children:n||o})]})};exports.RadioTileGroup=C;
@@ -0,0 +1,101 @@
1
+ import { default as React } from 'react';
2
+ import { VariantProps } from 'class-variance-authority';
3
+ import { radioTileVariants } from './radio-tile-group-variants';
4
+ export type RadioTileOption = {
5
+ value: string;
6
+ /** The option's name, under its picture. It's what the radio is called. */
7
+ label: string;
8
+ /** Supporting text under the name, read as the radio's description. */
9
+ description?: string;
10
+ /**
11
+ * What the tile shows above its name: a photo, a drawing, a swatch, any
12
+ * node. Hidden from assistive technology, since `label` already names the
13
+ * option, so the name isn't read twice. Give an image `alt=""`.
14
+ */
15
+ picture?: React.ReactNode;
16
+ disabled?: boolean;
17
+ };
18
+ /**
19
+ * How many tiles to a row: one count, or a count per breakpoint. A
20
+ * breakpoint left out takes the count of the one below it, and `base` is 1
21
+ * when it's left out.
22
+ */
23
+ export type RadioTileColumns = number | {
24
+ base?: number;
25
+ sm?: number;
26
+ md?: number;
27
+ lg?: number;
28
+ };
29
+ export interface RadioTileGroupProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue">, Pick<VariantProps<typeof radioTileVariants>, "variant"> {
30
+ options: RadioTileOption[];
31
+ /**
32
+ * The selected value. A value matching no option selects nothing, and
33
+ * leaves the first option as the group's tab stop: `""` is the way to say
34
+ * "not answered yet", as in `RadioGroup`.
35
+ */
36
+ value: string;
37
+ onChange: (value: string) => void;
38
+ /** The group's name. Becomes the group's accessible name. */
39
+ label?: string;
40
+ /** Supporting text under the tiles. */
41
+ helperText?: string;
42
+ /**
43
+ * The message for an invalid selection. Implies the error state, and is the
44
+ * group's description while it shows, as in `RadioGroup`, which says how
45
+ * `aria-invalid` and `aria-describedby` are handled.
46
+ */
47
+ errorMessage?: string;
48
+ /** Disables every tile, and dims the label and helper text with them. */
49
+ disabled?: boolean;
50
+ /** Says the group needs an answer, as `aria-required`. */
51
+ required?: boolean;
52
+ /**
53
+ * Tiles per row. Two, then three from the `sm` breakpoint, by default:
54
+ * two photos side by side is the useful size on a phone.
55
+ */
56
+ columns?: RadioTileColumns;
57
+ /**
58
+ * A tile's narrowest width, as a CSS length (`"9rem"`). Set, it replaces
59
+ * `columns`: each row holds as many tiles as fit the group's own width,
60
+ * whatever the viewport.
61
+ */
62
+ minTileWidth?: string;
63
+ /** Classes for the grid holding the tiles. `className` lands on the group. */
64
+ optionsClassName?: string;
65
+ /**
66
+ * Classes for every tile, the element carrying `role="radio"`. It carries
67
+ * `data-checked`, `data-disabled`, `data-hover` and `data-focus-visible` as
68
+ * well, so a chosen look can be drawn from them, with `variant="plain"` for
69
+ * none of Roster's own.
70
+ */
71
+ tileClassName?: string;
72
+ /** Classes for what holds each option's `picture`. */
73
+ pictureClassName?: string;
74
+ /** Classes for the group's own label. */
75
+ labelClassName?: string;
76
+ /** Classes for each option's name. */
77
+ optionLabelClassName?: string;
78
+ /** Classes for each option's `description`. */
79
+ descriptionClassName?: string;
80
+ /** Classes for the helper text or error message under the tiles. */
81
+ messageClassName?: string;
82
+ }
83
+ /**
84
+ * A radio group whose options are tiles: a picture, a name and an optional
85
+ * description each, with no radio dot. Choosing between photos, avatars,
86
+ * swatches or layouts.
87
+ *
88
+ * A sibling of `RadioGroup` rather than a variant of it. Built on the same
89
+ * Headless UI pieces, so it's the same radio group to a keyboard and a screen
90
+ * reader: one tab stop, the arrows moving and choosing (wrapping, skipping
91
+ * disabled tiles), Space choosing. It adds Home and End, to the first and
92
+ * last tile. `RadioGroup` keeps its own props, which are about dots and rows,
93
+ * and a form with only text radios doesn't carry the tiles' code.
94
+ *
95
+ * The arrows follow the tiles' order, not the grid's rows: Down moves to the
96
+ * next tile, as Right does, the order the tiles are read in. A row is however
97
+ * many tiles fit the width, so moving by row would change with the window,
98
+ * and the radio pattern a screen reader announces says Down is next.
99
+ */
100
+ declare const RadioTileGroup: ({ options, value, onChange, label, helperText, errorMessage, disabled, required, variant, columns, minTileWidth, className, optionsClassName, tileClassName, pictureClassName, labelClassName, optionLabelClassName, descriptionClassName, messageClassName, onKeyDown, "aria-invalid": ariaInvalid, ...props }: RadioTileGroupProps) => import("react/jsx-runtime").JSX.Element;
101
+ export { RadioTileGroup };
@@ -0,0 +1,142 @@
1
+ "use client";
2
+ import { jsxs as y, jsx as e } from "react/jsx-runtime";
3
+ import k from "react";
4
+ import { Description as h } from "../../../node_modules/@headlessui/react/dist/components/description/description.js";
5
+ import { Field as q } from "../../../node_modules/@headlessui/react/dist/components/field/field.js";
6
+ import { Label as g } from "../../../node_modules/@headlessui/react/dist/components/label/label.js";
7
+ import { RadioGroup as L, Radio as O } from "../../../node_modules/@headlessui/react/dist/components/radio-group/radio-group.js";
8
+ import { cn as s } from "../../../lib/utils.js";
9
+ import { isFieldInvalid as Z, withoutInvalidProps as z } from "../../../internal/field-invalid.js";
10
+ import { useKeyFocus as A } from "../../../internal/key-focus.js";
11
+ import { radioTilePictureVariants as B, radioTileVariants as C, radioTileGroupOptionsVariants as J } from "./radio-tile-group-variants.js";
12
+ const M = k.forwardRef(function(t, o) {
13
+ return /* @__PURE__ */ e("div", { ref: o, ...t, "data-focus-visible": t["data-focus"] });
14
+ });
15
+ function Q(a) {
16
+ const t = typeof a == "number" ? { base: a } : a, o = t.base ?? 1, l = t.sm ?? o, c = t.md ?? l, n = t.lg ?? c;
17
+ return {
18
+ "--rst-tiles-cols": o,
19
+ "--rst-tiles-cols-sm": l,
20
+ "--rst-tiles-cols-md": c,
21
+ "--rst-tiles-cols-lg": n
22
+ };
23
+ }
24
+ const ar = ({
25
+ options: a,
26
+ value: t,
27
+ onChange: o,
28
+ label: l,
29
+ helperText: c,
30
+ errorMessage: n,
31
+ disabled: d = !1,
32
+ required: w = !1,
33
+ variant: b = "outlined",
34
+ columns: N = { base: 2, sm: 3 },
35
+ minTileWidth: u,
36
+ className: R,
37
+ optionsClassName: D,
38
+ tileClassName: G,
39
+ pictureClassName: K,
40
+ labelClassName: P,
41
+ optionLabelClassName: V,
42
+ descriptionClassName: F,
43
+ messageClassName: T,
44
+ onKeyDown: j,
45
+ "aria-invalid": E,
46
+ ...p
47
+ }) => {
48
+ A();
49
+ const x = !!n, H = Z(x, { ...p, "aria-invalid": E }), I = z(p), v = b ?? "outlined", m = k.useRef(/* @__PURE__ */ new Map()), f = d ? [] : a.filter((r) => !r.disabled), _ = (r) => {
50
+ if (j?.(r), r.defaultPrevented || r.key !== "Home" && r.key !== "End") return;
51
+ const i = r.key === "Home" ? f[0] : f[f.length - 1];
52
+ i && (r.preventDefault(), m.current.get(i.value)?.focus(), i.value !== t && o(i.value));
53
+ };
54
+ return /* @__PURE__ */ y(
55
+ L,
56
+ {
57
+ value: t,
58
+ onChange: o,
59
+ disabled: d,
60
+ className: s("rst:font-ui rst:flex rst:flex-col rst:gap-1.5 rst:min-w-0", R),
61
+ ...I,
62
+ onKeyDown: _,
63
+ "aria-invalid": H || void 0,
64
+ "aria-required": w || void 0,
65
+ children: [
66
+ l && /* @__PURE__ */ e(
67
+ g,
68
+ {
69
+ className: s(
70
+ "rst:block rst:text-sm rst:font-medium rst:text-[var(--roster-control-text)] rst:text-left",
71
+ d && "rst:opacity-50",
72
+ P
73
+ ),
74
+ children: l
75
+ }
76
+ ),
77
+ /* @__PURE__ */ e(
78
+ "div",
79
+ {
80
+ className: s(J({ fit: u ? "width" : "columns" }), D),
81
+ style: u ? { "--rst-tiles-min": u } : Q(N),
82
+ children: a.map((r) => (
83
+ /* `disabled || option.disabled`, as in `RadioGroup`: Headless UI
84
+ takes the group's state only when the option's is `undefined`. */
85
+ /* @__PURE__ */ e(q, { disabled: d || r.disabled, className: "rst:flex rst:min-w-0", children: /* @__PURE__ */ y(
86
+ O,
87
+ {
88
+ as: M,
89
+ value: r.value,
90
+ ref: (i) => {
91
+ i ? m.current.set(r.value, i) : m.current.delete(r.value);
92
+ },
93
+ className: s(C({ variant: v }), G),
94
+ children: [
95
+ r.picture != null && /* @__PURE__ */ e("span", { "aria-hidden": "true", className: s(B({ variant: v }), K), children: r.picture }),
96
+ /* @__PURE__ */ e(
97
+ g,
98
+ {
99
+ as: "span",
100
+ passive: !0,
101
+ className: s(
102
+ "rst:block rst:text-sm rst:font-medium rst:text-[var(--roster-control-text)] rst:[overflow-wrap:anywhere]",
103
+ "rst:forced-colors:group-data-checked:underline rst:forced-colors:group-data-checked:decoration-2 rst:forced-colors:group-data-checked:underline-offset-4",
104
+ V
105
+ ),
106
+ children: r.label
107
+ }
108
+ ),
109
+ r.description && /* @__PURE__ */ e(
110
+ h,
111
+ {
112
+ as: "span",
113
+ className: s("rst:block rst:text-xs rst:text-gray-500 rst:dark:text-gray-400", F),
114
+ children: r.description
115
+ }
116
+ )
117
+ ]
118
+ }
119
+ ) }, r.value)
120
+ ))
121
+ }
122
+ ),
123
+ (c || n) && /* @__PURE__ */ e(
124
+ h,
125
+ {
126
+ as: "p",
127
+ className: s(
128
+ "rst:text-xs rst:text-left",
129
+ x ? "rst:text-error-600 rst:dark:text-error-400 rst:font-medium" : "rst:text-gray-500 rst:dark:text-gray-400",
130
+ d && "rst:opacity-50",
131
+ T
132
+ ),
133
+ children: n || c
134
+ }
135
+ )
136
+ ]
137
+ }
138
+ );
139
+ };
140
+ export {
141
+ ar as RadioTileGroup
142
+ };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("../../../node_modules/class-variance-authority/dist/index.cjs"),t=r.cva("rst:group rst:font-ui rst:relative rst:flex rst:h-full rst:w-full rst:min-w-0 rst:flex-col rst:gap-2 rst:rounded-lg rst:text-left rst:cursor-pointer rst:focus:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2 rst:ring-offset-background rst:data-disabled:cursor-not-allowed rst:data-disabled:opacity-50",{variants:{variant:{outlined:"rst:border rst:border-(--roster-tile-border) rst:bg-(--roster-tile-bg) rst:p-2 rst:data-checked:border-(--roster-tile-border-checked) rst:data-checked:inset-ring rst:data-checked:inset-ring-(--roster-tile-border-checked) rst:data-checked:bg-(--roster-tile-bg-checked) rst:forced-colors:data-checked:border-2",plain:""}},defaultVariants:{variant:"outlined"}}),s=r.cva("rst:block rst:w-full rst:min-w-0",{variants:{variant:{outlined:"rst:overflow-hidden rst:rounded-md rst:[&>img]:block rst:[&>img]:aspect-square rst:[&>img]:w-full rst:[&>img]:object-cover",plain:""}},defaultVariants:{variant:"outlined"}}),e=r.cva("rst:grid rst:gap-3",{variants:{fit:{columns:"rst:grid-cols-[repeat(var(--rst-tiles-cols),minmax(0,1fr))] rst:sm:grid-cols-[repeat(var(--rst-tiles-cols-sm),minmax(0,1fr))] rst:md:grid-cols-[repeat(var(--rst-tiles-cols-md),minmax(0,1fr))] rst:lg:grid-cols-[repeat(var(--rst-tiles-cols-lg),minmax(0,1fr))]",width:"rst:grid-cols-[repeat(auto-fill,minmax(min(var(--rst-tiles-min),100%),1fr))]"}},defaultVariants:{fit:"columns"}});exports.radioTileGroupOptionsVariants=e;exports.radioTilePictureVariants=s;exports.radioTileVariants=t;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * One tile: the whole tile is the radio, with no dot.
3
+ *
4
+ * `outlined` is Roster's own look, and a quiet one: a hairline in
5
+ * `--roster-tile-border` at rest, and the chosen tile's edge in
6
+ * `--roster-tile-border-checked`, drawn two pixels wide (the border and an
7
+ * inset ring) over the faintest tint, `--roster-tile-bg-checked`. The edge
8
+ * changes weight as well as hue, so the choice doesn't rest on color alone.
9
+ *
10
+ * `plain` draws nothing: no edge, no fill, no padding, in any state. It's for
11
+ * a consumer drawing its own chosen look from the tile's `data-checked`, a
12
+ * printed border or a stamp, without first undoing Roster's.
13
+ *
14
+ * Both keep the focus ring, Roster's, on `focus-visible`. A box-shadow ring
15
+ * offset from the tile's border box, so it sits outside anything a consumer
16
+ * draws on the tile or inside it. And both dim a disabled tile.
17
+ *
18
+ * Forced colors drops box-shadows and repaints every edge and fill alike, so
19
+ * there the chosen tile's name is underlined (in both looks, set where the
20
+ * name is drawn), and the `outlined` edge goes to two pixels: the same cues
21
+ * `Pagination` gives its current page.
22
+ */
23
+ export declare const radioTileVariants: (props?: ({
24
+ variant?: "plain" | "outlined" | null | undefined;
25
+ } & import('class-variance-authority/types').ClassProp) | undefined) => string;
26
+ /**
27
+ * What holds an option's `picture`.
28
+ *
29
+ * `outlined` rounds and clips it, and gives an `<img>` placed directly inside
30
+ * a square, cover-fitted box the tile's width, so photos of any shape make
31
+ * even rows. Anything else, a drawing or a swatch, keeps its own shape. `plain`
32
+ * neither clips nor shapes: a consumer's frame, and a stamp hung over its
33
+ * corner, show whole.
34
+ */
35
+ export declare const radioTilePictureVariants: (props?: ({
36
+ variant?: "plain" | "outlined" | null | undefined;
37
+ } & import('class-variance-authority/types').ClassProp) | undefined) => string;
38
+ /**
39
+ * The tiles' grid.
40
+ *
41
+ * `columns` reads a count per breakpoint from custom properties the component
42
+ * sets, so any count works without a class per number. `width` fills each row
43
+ * with as many tiles as fit at a minimum width, which follows the space the
44
+ * group is given rather than the viewport, and never lets a tile overflow a
45
+ * container narrower than that minimum.
46
+ */
47
+ export declare const radioTileGroupOptionsVariants: (props?: ({
48
+ fit?: "columns" | "width" | null | undefined;
49
+ } & import('class-variance-authority/types').ClassProp) | undefined) => string;
@@ -0,0 +1,34 @@
1
+ import { cva as r } from "../../../node_modules/class-variance-authority/dist/index.js";
2
+ const s = /* @__PURE__ */ r(
3
+ "rst:group rst:font-ui rst:relative rst:flex rst:h-full rst:w-full rst:min-w-0 rst:flex-col rst:gap-2 rst:rounded-lg rst:text-left rst:cursor-pointer rst:focus:outline-hidden rst:focus-visible:ring-2 rst:focus-visible:ring-ring rst:focus-visible:ring-offset-2 rst:ring-offset-background rst:data-disabled:cursor-not-allowed rst:data-disabled:opacity-50",
4
+ {
5
+ variants: {
6
+ variant: {
7
+ outlined: "rst:border rst:border-(--roster-tile-border) rst:bg-(--roster-tile-bg) rst:p-2 rst:data-checked:border-(--roster-tile-border-checked) rst:data-checked:inset-ring rst:data-checked:inset-ring-(--roster-tile-border-checked) rst:data-checked:bg-(--roster-tile-bg-checked) rst:forced-colors:data-checked:border-2",
8
+ plain: ""
9
+ }
10
+ },
11
+ defaultVariants: { variant: "outlined" }
12
+ }
13
+ ), e = /* @__PURE__ */ r("rst:block rst:w-full rst:min-w-0", {
14
+ variants: {
15
+ variant: {
16
+ outlined: "rst:overflow-hidden rst:rounded-md rst:[&>img]:block rst:[&>img]:aspect-square rst:[&>img]:w-full rst:[&>img]:object-cover",
17
+ plain: ""
18
+ }
19
+ },
20
+ defaultVariants: { variant: "outlined" }
21
+ }), i = /* @__PURE__ */ r("rst:grid rst:gap-3", {
22
+ variants: {
23
+ fit: {
24
+ columns: "rst:grid-cols-[repeat(var(--rst-tiles-cols),minmax(0,1fr))] rst:sm:grid-cols-[repeat(var(--rst-tiles-cols-sm),minmax(0,1fr))] rst:md:grid-cols-[repeat(var(--rst-tiles-cols-md),minmax(0,1fr))] rst:lg:grid-cols-[repeat(var(--rst-tiles-cols-lg),minmax(0,1fr))]",
25
+ width: "rst:grid-cols-[repeat(auto-fill,minmax(min(var(--rst-tiles-min),100%),1fr))]"
26
+ }
27
+ },
28
+ defaultVariants: { fit: "columns" }
29
+ });
30
+ export {
31
+ i as radioTileGroupOptionsVariants,
32
+ e as radioTilePictureVariants,
33
+ s as radioTileVariants
34
+ };
@@ -1 +1 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("react/jsx-runtime"),l=require("../../../lib/utils.cjs"),h=require("../../../lib/labels.cjs"),b=require("../../atoms/Progress/Progress.cjs"),y={nav:"Progress",position:(s,c)=>`Step ${s} of ${c}`,done:"done",number:s=>String(s)},j=()=>r.jsx("svg",{viewBox:"0 0 24 24",width:"16",height:"16",fill:"none",stroke:"currentColor",strokeWidth:"3",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:r.jsx("path",{d:"M20 6 9 17l-5-5"})});function k({steps:s,current:c,variant:g="stepper",labels:f,className:p}){const o=h.withLabels(y,f),n=s.length;if(n===0)return null;const t=Math.min(Math.max(1,Math.floor(Number.isFinite(c)?c:1)),n+1),d=t>n,u=d?o.position(n,n):o.position(t,n),x=d?u:`${u}: ${s[t-1]}`;return g==="segmented"?r.jsxs("div",{className:l.cn("rst:font-ui rst:w-full rst:min-w-0",p),"data-step-progress":"segmented",children:[r.jsxs("div",{className:"rst:mb-1.5 rst:flex rst:items-baseline rst:justify-between rst:gap-3 rst:text-sm","aria-hidden":"true",children:[r.jsx("span",{className:"rst:font-medium rst:text-[var(--roster-control-text)]","data-step-position":"",children:u}),!d&&r.jsx("span",{className:"rst:min-w-0 rst:truncate rst:text-gray-600 rst:dark:text-gray-400","data-step-name":"",children:s[t-1]})]}),r.jsx(b.Progress,{variant:"segmented",segments:s.map((m,i)=>i+1<t?"done":i+1===t?"current":"remaining"),announce:!1}),r.jsx("span",{role:"status",className:"rst:sr-only","data-step-status":"",children:x})]}):r.jsxs("div",{className:l.cn("rst:font-ui rst:w-full",p),"data-step-progress":"stepper",children:[r.jsx("ol",{"aria-label":o.nav,className:"rst:m-0 rst:flex rst:w-full rst:list-none rst:p-0",children:s.map((m,i)=>{const a=i+1,e=a<t?"done":a===t?"current":"upcoming";return r.jsxs("li",{"aria-current":e==="current"?"step":void 0,className:"rst:relative rst:flex rst:min-w-0 rst:flex-1 rst:flex-col rst:items-center rst:gap-1.5 rst:text-center","data-step":e,children:[i>0&&r.jsx("span",{"aria-hidden":"true",className:l.cn("rst:absolute rst:right-1/2 rst:top-4 rst:h-0.5 rst:w-full rst:-translate-y-1/2",a<=t?"rst:bg-primary-600 rst:dark:bg-primary-400 rst:forced-colors:forced-color-adjust-none rst:forced-colors:bg-[Highlight]":"rst:bg-[var(--roster-skeleton)] rst:forced-colors:forced-color-adjust-none rst:forced-colors:bg-[GrayText]"),"data-step-connector":a<=t?"filled":"empty"}),r.jsx("span",{"aria-hidden":"true",className:l.cn("rst:relative rst:z-10 rst:flex rst:h-8 rst:w-8 rst:items-center rst:justify-center rst:rounded-full rst:text-sm rst:font-semibold rst:tabular-nums",e==="done"&&"rst:bg-primary-600 rst:text-primary-600-ink rst:dark:bg-primary-400 rst:dark:text-gray-950 rst:forced-colors:forced-color-adjust-none rst:forced-colors:bg-[Highlight] rst:forced-colors:text-[HighlightText]",e==="current"&&"rst:border-2 rst:border-primary-600 rst:bg-[var(--roster-card-bg)] rst:text-primary-600 rst:dark:border-primary-400 rst:dark:text-primary-400",e==="upcoming"&&"rst:bg-[var(--roster-skeleton)] rst:text-gray-600 rst:dark:text-gray-300 rst:forced-colors:border"),"data-step-marker":"",children:e==="done"?r.jsx(j,{}):o.number(a)}),r.jsxs("span",{className:l.cn("rst:w-full rst:px-1 rst:text-xs rst:leading-4 rst:break-words",e==="current"?"rst:font-semibold rst:text-[var(--roster-control-text)]":"rst:text-gray-600 rst:dark:text-gray-400"),children:[m,e==="done"&&r.jsx("span",{className:"rst:sr-only",children:`, ${o.done}`})]})]},`${a}-${m}`)})}),r.jsx("span",{role:"status",className:"rst:sr-only","data-step-status":"",children:x})]})}exports.StepProgress=k;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("react/jsx-runtime"),c=require("../../../lib/utils.cjs"),h=require("../../../lib/labels.cjs"),b=require("../../atoms/Progress/Progress.cjs"),j=require("../../../internal/step-marker.cjs"),y=require("../../../internal/step-check.cjs"),N={nav:"Progress",position:(r,i)=>`Step ${r} of ${i}`,done:"done",number:r=>String(r)};function v({steps:r,current:i,variant:g="stepper",labels:f,className:m}){const o=h.withLabels(N,f),e=r.length;if(e===0)return null;const t=Math.min(Math.max(1,Math.floor(Number.isFinite(i)?i:1)),e+1),d=t>e,u=d?o.position(e,e):o.position(t,e),x=d?u:`${u}: ${r[t-1]}`;return g==="segmented"?s.jsxs("div",{className:c.cn("rst:font-ui rst:w-full rst:min-w-0",m),"data-step-progress":"segmented",children:[s.jsxs("div",{className:"rst:mb-1.5 rst:flex rst:items-baseline rst:justify-between rst:gap-3 rst:text-sm","aria-hidden":"true",children:[s.jsx("span",{className:"rst:font-medium rst:text-[var(--roster-control-text)]","data-step-position":"",children:u}),!d&&s.jsx("span",{className:"rst:min-w-0 rst:truncate rst:text-gray-600 rst:dark:text-gray-400","data-step-name":"",children:r[t-1]})]}),s.jsx(b.Progress,{variant:"segmented",segments:r.map((p,l)=>l+1<t?"done":l+1===t?"current":"remaining"),announce:!1}),s.jsx("span",{role:"status",className:"rst:sr-only","data-step-status":"",children:x})]}):s.jsxs("div",{className:c.cn("rst:font-ui rst:w-full",m),"data-step-progress":"stepper",children:[s.jsx("ol",{"aria-label":o.nav,className:"rst:m-0 rst:flex rst:w-full rst:list-none rst:p-0",children:r.map((p,l)=>{const n=l+1,a=n<t?"done":n===t?"current":"upcoming";return s.jsxs("li",{"aria-current":a==="current"?"step":void 0,className:"rst:relative rst:flex rst:min-w-0 rst:flex-1 rst:flex-col rst:items-center rst:gap-1.5 rst:text-center","data-step":a,children:[l>0&&s.jsx("span",{"aria-hidden":"true",className:c.cn("rst:absolute rst:right-1/2 rst:top-4 rst:h-0.5 rst:w-full rst:-translate-y-1/2",n<=t?"rst:bg-primary-600 rst:dark:bg-primary-400 rst:forced-colors:forced-color-adjust-none rst:forced-colors:bg-[Highlight]":"rst:bg-[var(--roster-skeleton)] rst:forced-colors:forced-color-adjust-none rst:forced-colors:bg-[GrayText]"),"data-step-connector":n<=t?"filled":"empty"}),s.jsx("span",{"aria-hidden":"true",className:j.stepMarkerClass(a),"data-step-marker":"",children:a==="done"?s.jsx(y.StepCheck,{}):o.number(n)}),s.jsxs("span",{className:c.cn("rst:w-full rst:px-1 rst:text-xs rst:leading-4 rst:break-words",a==="current"?"rst:font-semibold rst:text-[var(--roster-control-text)]":"rst:text-gray-600 rst:dark:text-gray-400"),children:[p,a==="done"&&s.jsx("span",{className:"rst:sr-only",children:`, ${o.done}`})]})]},`${n}-${p}`)})}),s.jsx("span",{role:"status",className:"rst:sr-only","data-step-status":"",children:x})]})}exports.StepProgress=v;