@phreshos/react-ui 0.1.29 → 0.1.31

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.
Files changed (84) hide show
  1. package/README.md +197 -39
  2. package/dist/accordion.d.ts +45 -0
  3. package/dist/accordion.js +62 -0
  4. package/dist/alert-dialog.d.ts +0 -1
  5. package/dist/alert-dialog.js +0 -1
  6. package/dist/aria-direction.d.ts +9 -0
  7. package/dist/aria-direction.js +49 -0
  8. package/dist/calendar-layout.d.ts +8 -0
  9. package/dist/calendar-layout.js +103 -0
  10. package/dist/calendar-trigger.d.ts +5 -0
  11. package/dist/calendar-trigger.js +30 -0
  12. package/dist/calendar.d.ts +18 -0
  13. package/dist/calendar.js +13 -0
  14. package/dist/color.d.ts +16 -1
  15. package/dist/color.js +60 -11
  16. package/dist/combo-box.d.ts +20 -0
  17. package/dist/combo-box.js +68 -0
  18. package/dist/context-menu.d.ts +2 -4
  19. package/dist/context-menu.js +0 -1
  20. package/dist/control-surface.d.ts +2 -1
  21. package/dist/control-surface.js +2 -2
  22. package/dist/control.d.ts +1 -1
  23. package/dist/control.js +7 -8
  24. package/dist/date-control.d.ts +24 -0
  25. package/dist/date-control.js +69 -0
  26. package/dist/date-field.d.ts +19 -0
  27. package/dist/date-field.js +13 -0
  28. package/dist/date-picker.d.ts +19 -0
  29. package/dist/date-picker.js +16 -0
  30. package/dist/date-range-picker.d.ts +20 -0
  31. package/dist/date-range-picker.js +25 -0
  32. package/dist/date-range.d.ts +6 -0
  33. package/dist/date-range.js +0 -0
  34. package/dist/dialog.d.ts +4 -2
  35. package/dist/dialog.js +24 -7
  36. package/dist/disclosure.d.ts +42 -0
  37. package/dist/disclosure.js +97 -0
  38. package/dist/dropdown-menu.d.ts +0 -2
  39. package/dist/dropdown-menu.js +0 -1
  40. package/dist/list-box.d.ts +49 -0
  41. package/dist/list-box.js +129 -0
  42. package/dist/main.d.ts +27 -11
  43. package/dist/main.js +26 -10
  44. package/dist/material-options.d.ts +4 -2
  45. package/dist/material-paint.js +19 -1
  46. package/dist/menu.d.ts +22 -4
  47. package/dist/menu.js +25 -6
  48. package/dist/panel.d.ts +0 -1
  49. package/dist/panel.js +0 -1
  50. package/dist/popover.d.ts +0 -1
  51. package/dist/popover.js +13 -3
  52. package/dist/progress-bar.d.ts +22 -0
  53. package/dist/progress-bar.js +46 -0
  54. package/dist/radio.d.ts +5 -5
  55. package/dist/radio.js +8 -4
  56. package/dist/radius.js +1 -1
  57. package/dist/range-calendar.d.ts +19 -0
  58. package/dist/range-calendar.js +15 -0
  59. package/dist/scroll-area.d.ts +4 -1
  60. package/dist/scroll-area.js +3 -3
  61. package/dist/select.js +4 -14
  62. package/dist/selection.d.ts +18 -0
  63. package/dist/selection.js +28 -0
  64. package/dist/slider.js +1 -1
  65. package/dist/spacing.js +1 -1
  66. package/dist/surface.d.ts +1 -1
  67. package/dist/surface.js +22 -3
  68. package/dist/table.d.ts +78 -0
  69. package/dist/table.js +148 -0
  70. package/dist/tabs.d.ts +45 -0
  71. package/dist/tabs.js +103 -0
  72. package/dist/time-field.d.ts +22 -0
  73. package/dist/time-field.js +13 -0
  74. package/dist/toggle-indicator.js +1 -1
  75. package/dist/toolbar.d.ts +46 -0
  76. package/dist/toolbar.js +121 -0
  77. package/dist/tooltip.d.ts +0 -1
  78. package/dist/tooltip.js +0 -1
  79. package/dist/tree.d.ts +56 -0
  80. package/dist/tree.js +134 -0
  81. package/dist/window.d.ts +39 -0
  82. package/dist/{window-header.js → window.js} +57 -31
  83. package/package.json +4 -2
  84. package/dist/window-header.d.ts +0 -42
package/README.md CHANGED
@@ -74,14 +74,20 @@ function Example() {
74
74
  }
75
75
  ```
76
76
 
77
- `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, and `Radio`
77
+ `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, and `RadioGroup.Item`
78
78
  use the shared Surface implementation while retaining their native behavior. Color and material
79
79
  are independent inputs. `color` accepts an Appearance color and resting level such
80
80
  as `background:base` or `primary:soft`, or a direct CSS color. Surface defaults
81
81
  to `background:base`; solid controls default to `default:base`.
82
- Hover and press derive shades of that fill. Surface and solid controls choose
83
- the higher-contrast text from Appearance's background and foreground whenever
84
- their fill can be evaluated; interaction shades keep that choice.
82
+ Every independently owned base paint is exposed through its component's
83
+ `color` prop. Hover, press, focus, selection, and inactive treatments derive
84
+ from that base rather than introducing separate color inputs. Surface and solid controls compare
85
+ the fill with an adaptive perceptual boundary biased toward lighter text, then
86
+ use the Appearance candidate on the opposite side; interaction shades keep that
87
+ choice.
88
+ The omitted value follows the component's meaning: neutral controls use
89
+ `default:base`, while a role with inherent visual meaning may choose another
90
+ Appearance color. Every semantic default remains overridable through `color`.
85
91
  `size` accepts `xsmall`, `small`, `medium` (default), `large`, or `xlarge`;
86
92
  spacing and radius follow Appearance, while typography remains inherited and
87
93
  component sizes are relative to the surrounding text. `disabled` prevents activation and focus;
@@ -115,11 +121,18 @@ required geometry.
115
121
  `MaterialOptions` defines `opacity`, `backdrop`, `grain`, `grainAmount`,
116
122
  `distortion`, and `saturation`. Every material-bearing component, including
117
123
  Surface, exposes these values through its `material` prop. Color remains a
118
- separate property. Omission or `true` uses `appearance.material`; `false`
119
- removes the material and paints the resolved color as a normal background.
120
- Effect options accept a scale level or a direct number;
121
- opacity affects Surface paint only, never its content. The material edge is part
122
- of the same Surface rather than a second public entity.
124
+ separate property. Omission uses the resource-efficient `basic` rendering mode.
125
+ `none` ignores Material and paints the resolved color as a normal background;
126
+ `basic` adds Material paint, grain, and its edge while remaining solid, with
127
+ Material opacity applied only to the grain; `extended` applies that opacity to
128
+ the complete Material instead; and `full` adds backdrop, distortion, and
129
+ saturation. A `MaterialOptions` object selects full rendering with those
130
+ overrides. Effect options accept a scale level or a direct number; opacity
131
+ affects Surface paint only, never its content. The material edge is part of the
132
+ same Surface rather than a second public entity.
133
+
134
+ The modes are progressive: each level preserves the rendering of the previous
135
+ one and adds only its own Material capability.
123
136
 
124
137
  Surface-based controls expose the same separate `color` and `material` props.
125
138
  `ShadowOptions` similarly groups `x`, `y`, `blur`, `spread`, and `opacity` under
@@ -128,12 +141,13 @@ branch, while `false` removes the shadow. Each option accepts a scale level or
128
141
  direct number. The shadow is a neutral black
129
142
  outer shadow. Surface-based controls expose the same `shadow` prop.
130
143
  For text fields and Select, `material` targets the field or trigger; for Checkbox,
131
- Switch, and Radio, it targets the indicator. RadioGroup supplies material defaults
132
- to its options, and a Radio can override them. Shadow follows the same targets and
144
+ Switch, and RadioGroup.Item, it targets the indicator. RadioGroup supplies material defaults
145
+ to its options, and an Item can override them. Shadow follows the same targets and
133
146
  inheritance path.
134
147
 
135
148
  ```tsx
136
- <Button color="primary:base" material={{ opacity: 0.6, backdrop: 0 }}>Save</Button>
149
+ <Button color="primary:base" material="full">Save</Button>
150
+ <Surface material={{ opacity: 0.6, backdrop: 0 }}>Translucent</Surface>
137
151
  <Input label="Name" radius="large" material={{ grain: "small" }} shadow={{ blur: "small" }} />
138
152
  ```
139
153
 
@@ -141,6 +155,11 @@ inheritance path.
141
155
  `Surface`. Both materials retain Surface defaults; the content inset follows
142
156
  Appearance spacing. Positioning, modality, and lifecycle belong to the caller.
143
157
 
158
+ Component families use one public path: the family name is the root and parts
159
+ that depend on it are properties of that family. For example, use
160
+ `Panel.Header` and `Dialog.Title`; those parts are not separate top-level
161
+ components. Components with independent contracts remain top-level exports.
162
+
144
163
  ```tsx
145
164
  import { Panel } from "@phreshos/react-ui"
146
165
 
@@ -154,23 +173,29 @@ Native properties and the root ref target the outer Surface. `Panel.Header`
154
173
  owns the optional leading region, while `Panel.Content` is the independently
155
174
  configurable inner Surface.
156
175
 
157
- `WindowHeader` provides the layout shared by Desktop-owned and application-owned
158
- window headers. `Identity` presents the icon and truncating title, `Center` is
159
- optional flexible space, and `Actions` aligns compact controls at the end. The
160
- header uses the enclosing Surface rather than creating another material layer.
161
- Its controls are React UI Buttons; callers connect them to their own window
162
- operations. Pointer and double-click handlers on the root can implement a drag
163
- area, while the center and actions keep their interactions separate.
176
+ `Window` provides one Surface with a ready header-and-content layout.
177
+ `Window.Header.Identity` presents the icon and truncating title,
178
+ `Window.Header.Center` is optional flexible space, and
179
+ `Window.Header.Actions` aligns compact controls at the end. `Window.Content`
180
+ fills the remaining area without deciding its overflow behavior. The header
181
+ uses the enclosing Surface rather than creating another material layer, and it
182
+ can also be composed independently inside any Surface. Its controls are React
183
+ UI Buttons; callers connect them to their own window operations. Pointer and
184
+ double-click handlers on the header can implement a drag area, while the center
185
+ and actions keep their interactions separate.
164
186
 
165
187
  ```tsx
166
- <WindowHeader active={active} onPointerDown={beginDrag}>
167
- <WindowHeader.Identity icon={icon} title={title} />
168
- <WindowHeader.Actions>
169
- <WindowHeader.Minimize onPress={minimize} />
170
- <WindowHeader.Maximize maximized={maximized} onPress={toggleMaximize} />
171
- <WindowHeader.Close onPress={close} />
172
- </WindowHeader.Actions>
173
- </WindowHeader>
188
+ <Window>
189
+ <Window.Header active={active} onPointerDown={beginDrag}>
190
+ <Window.Header.Identity icon={icon} title={title} />
191
+ <Window.Header.Actions>
192
+ <Window.Header.Minimize onPress={minimize} />
193
+ <Window.Header.Maximize maximized={maximized} onPress={toggleMaximize} />
194
+ <Window.Header.Close onPress={close} />
195
+ </Window.Header.Actions>
196
+ </Window.Header>
197
+ <Window.Content>{children}</Window.Content>
198
+ </Window>
174
199
  ```
175
200
 
176
201
  ## Overlays
@@ -193,6 +218,9 @@ semantic content remains explicit.
193
218
 
194
219
  `Popover` presents anchored non-modal content. `DropdownMenu` and
195
220
  `ContextMenu` open the same `Menu` contract through different interactions.
221
+ Menu is selection-free by default. Selectable menus use string identities
222
+ through `value`, `defaultValue`, `onChange`, and `disabledValues`, matching the
223
+ other selectable collections; root-level command dispatch uses `onItemAction`.
196
224
  `Dialog` blocks interaction behind it and may be dismissable, while
197
225
  `AlertDialog` requires an explicit decision by default. `Tooltip` supplies a
198
226
  short description on focus or hover. Portal cleanup, focus restoration,
@@ -205,8 +233,8 @@ Every input uses Appearance colors and the same five `size` levels as Button.
205
233
  `color` selects the fill independently from material. Input, Textarea, Select,
206
234
  and Button use `default:base` when it is omitted. Invalid
207
235
  fields use danger instead. Text fields share Surface's glass edge. Interaction shades
208
- derive from the base color's lightness, and text or selection marks use whichever
209
- Appearance background or foreground has higher contrast against the base fill.
236
+ derive from the base color's lightness, and text or selection marks use the Appearance
237
+ background or foreground that is perceptually opposite the base fill.
210
238
  That text or mark color stays unchanged across interaction shades.
211
239
  These are component-owned derivations, not additional Appearance settings.
212
240
  Theme names never imply particular colors. No component requires a Client or
@@ -231,10 +259,19 @@ behavior are never delayed by visual animation.
231
259
  | --- | --- | --- |
232
260
  | `Input` | `value`, `defaultValue`, `onChange(string)` | Single-line text; supports text, email, password, search, URL, and telephone types |
233
261
  | `Textarea` | `value`, `defaultValue`, `onChange(string)` | Multiline text; four rows by default, vertically resizable |
262
+ | `DateField` | `CalendarDate \| null` through `value`, `defaultValue`, and `onChange` | A locale-aware date whose year, month, and day are edited as independent segments |
263
+ | `TimeField` | `Time \| null` through `value`, `defaultValue`, and `onChange` | A locale-aware clock time whose visible units are edited as independent segments |
264
+ | `Calendar` | `CalendarDate \| null` selection | A keyboard-navigable month view for choosing one date |
265
+ | `RangeCalendar` | `DateRange \| null` selection | A month view for choosing an inclusive start and end date |
266
+ | `DatePicker` | The same `CalendarDate \| null` contract as DateField | Segmented date entry with a calendar selection popover |
267
+ | `DateRangePicker` | The same `DateRange \| null` contract as RangeCalendar | Two segmented date fields with a shared range calendar popover |
234
268
  | `Checkbox` | `checked`, `defaultChecked`, `onChange(boolean)` | Independent selection; `indeterminate` represents a mixed state |
235
269
  | `Switch` | `checked`, `defaultChecked`, `onChange(boolean)` | An on/off setting |
236
- | `RadioGroup` / `Radio` | Group `value`, `defaultValue`, `onChange(string)`; Radio `value` | One exclusive choice; vertical by default, optionally horizontal |
270
+ | `RadioGroup` / `RadioGroup.Item` | Group `value`, `defaultValue`, `onChange(string)`; Item `value` | One exclusive choice; vertical by default, optionally horizontal |
237
271
  | `Select` | `value: string \| null`, `defaultValue`, `onChange(string \| null)` | One choice from `options: { value, label, disabled? }[]` |
272
+ | `ComboBox` | Selected `value: string \| null` and query `inputValue: string` | One searchable choice from `options: { value, label, disabled? }[]` |
273
+ | `ListBox` | Single string or multiple string-array `value`, `defaultValue`, and `onChange` | A visible selectable collection with composable items and sections |
274
+ | `Tree` | Optional selection plus controlled or uncontrolled string-array expansion | Nested hierarchical navigation with arbitrary branch depth |
238
275
  | `Slider` | `value`, `defaultValue`, `onChange(number)` | One numeric value; `minValue`, `maxValue`, `step`, and `onChangeEnd`; horizontal by default |
239
276
 
240
277
  Use `label` for visible labels, or `aria-label` / `aria-labelledby` for an
@@ -243,44 +280,165 @@ accessible name without visible text. `description` provides associated help.
243
280
  `name` participates in native form submission. Controlled values remain owned
244
281
  by the caller; omit them and use defaults for internal state and form reset.
245
282
 
246
- Text fields, Checkbox, Switch, and RadioGroup also support `readOnly`: the
283
+ Text fields, ComboBox, Checkbox, Switch, and RadioGroup also support `readOnly`: the
247
284
  value cannot change, but the control remains focusable. These fields and
248
285
  Select support `required`, `invalid`, `errorMessage`, and React Aria's native
249
- or ARIA validation behavior. A Radio's selection and validation belong to its
286
+ or ARIA validation behavior. A RadioGroup Item's selection and validation belong to its
250
287
  group. Slider represents a bounded number rather than a required text or
251
288
  choice field, and has no read-only or validation-error mode. Select has no
252
289
  read-only mode; disable it when selection must be unavailable.
253
290
 
254
- `Input`, `Textarea`, and `Select` accept `radius`. Other inputs retain their
255
- intrinsic indicator shapes. `className` and `style` address the field's root;
291
+ `Input`, `Textarea`, `DateField`, `TimeField`, `DatePicker`, `DateRangePicker`,
292
+ `Calendar`, `RangeCalendar`, `Select`, and `ComboBox` accept `radius`. Other inputs
293
+ retain their intrinsic indicator shapes. `className` and `style` address the field's root;
256
294
  default visual properties remain component-owned. Input and Textarea refs
257
295
  target their native text controls; other refs target the root div. Checkbox,
258
- Switch, and Radio additionally accept `inputRef` for their native input.
296
+ Switch, and RadioGroup.Item additionally accept `inputRef` for their native input.
259
297
 
260
298
  ```tsx
261
- import { Input, Textarea, Checkbox, Radio, RadioGroup, Switch, Select, Slider } from "@phreshos/react-ui"
299
+ import { Input, Textarea, DateField, TimeField, Calendar, RangeCalendar, DatePicker, DateRangePicker, Checkbox, RadioGroup, Switch, Select, ComboBox, ListBox, Tree, Slider } from "@phreshos/react-ui"
300
+ import { parseDate, parseTime } from "@internationalized/date"
262
301
 
263
302
  <Input label="Name" name="name" required />
264
303
  <Textarea label="Description" name="description" />
304
+ <DateField label="Due date" name="dueDate" defaultValue={parseDate("2026-09-21")} />
305
+ <TimeField label="Start time" name="startTime" defaultValue={parseTime("09:30")} />
306
+ <DatePicker label="Appointment" name="appointment" defaultValue={parseDate("2026-09-21")} />
307
+ <Calendar aria-label="Release date" defaultValue={parseDate("2026-09-21")} />
308
+ <RangeCalendar aria-label="Trip dates" defaultValue={{ start: parseDate("2026-09-21"), end: parseDate("2026-09-24") }} />
309
+ <DateRangePicker label="Trip dates" defaultValue={{ start: parseDate("2026-09-21"), end: parseDate("2026-09-24") }} />
265
310
  <Checkbox label="Remember this choice" name="remember" />
266
311
  <Switch label="Notifications" name="notifications" defaultChecked />
267
312
  <RadioGroup label="Layout" name="layout" defaultValue="grid">
268
- <Radio label="Grid" value="grid" />
269
- <Radio label="List" value="list" />
313
+ <RadioGroup.Item label="Grid" value="grid" />
314
+ <RadioGroup.Item label="List" value="list" />
270
315
  </RadioGroup>
271
316
  <Select label="Sort" name="sort" options={[
272
317
  { value: "name", label: "Name" },
273
318
  { value: "date", label: "Date" }
274
319
  ]} />
320
+ <ComboBox label="Region" name="region" options={[
321
+ { value: "eu", label: "Europe" },
322
+ { value: "us", label: "United States" }
323
+ ]} />
324
+ <ListBox aria-label="Programs" defaultValue="editor">
325
+ <ListBox.Section id="productivity">
326
+ <ListBox.Header>Productivity</ListBox.Header>
327
+ <ListBox.Item id="editor">Editor</ListBox.Item>
328
+ <ListBox.Item id="terminal">Terminal</ListBox.Item>
329
+ </ListBox.Section>
330
+ </ListBox>
331
+ <Tree aria-label="Project files" defaultExpanded={["source"]}>
332
+ <Tree.Item id="source" textValue="Source">
333
+ <Tree.Content>Source</Tree.Content>
334
+ <Tree.Item id="main" textValue="main.ts">
335
+ <Tree.Content>main.ts</Tree.Content>
336
+ </Tree.Item>
337
+ </Tree.Item>
338
+ </Tree>
275
339
  <Slider label="Volume" name="volume" defaultValue={50} minValue={0} maxValue={100} step={1} />
276
340
  ```
277
341
 
342
+ `ComboBox` filters option labels as the user types. Selection uses `value` and
343
+ `onChange`; its query is independently controllable through `inputValue` and
344
+ `onInputChange`. Use `defaultValue` and `defaultInputValue` for uncontrolled
345
+ state.
346
+
347
+ `ListBox` uses single selection by default and accepts `selectionMode="multiple"`
348
+ for a string array or `"all"`. Its Items require stable string identities.
349
+ Sections group options under Headers; they do not introduce expandable hierarchy.
350
+ ListBox owns collection layout, selection, focus, keyboard behavior, and item
351
+ presentation. An enclosing Surface or ScrollArea remains an explicit consumer
352
+ composition.
353
+
354
+ `Tree` represents hierarchy rather than grouping. Branches may contain other
355
+ branches to any depth. Expansion uses `expanded`, `defaultExpanded`, and
356
+ `onExpandedChange`, all expressed as string identities. Selection is disabled
357
+ by default and uses the same explicit single- or multiple-selection contract as
358
+ Table when enabled. `Tree.Content` supplies each row's visible content and its
359
+ accessible disclosure control; `Tree.Collection` recursively renders
360
+ data-backed child collections.
361
+
278
362
  Each control delegates focus, keyboard, form, and selection behavior to its
279
363
  accessible behavioral primitive rather than recreating those systems. Select's
280
- popup uses Surface defaults. Toggle indicators use Motion internally and respect
364
+ popup and ComboBox's searchable popup use Surface defaults. Toggle indicators use Motion internally and respect
281
365
  animation preferences. The preview Program demonstrates each input's
282
366
  sizes, colors, state, and interaction without overriding its visual defaults.
283
367
 
368
+ ## Tabs
369
+
370
+ `Tabs` organizes related peer views under one selected string identity. Its
371
+ horizontal list divides the available width equally, while its active indicator
372
+ moves continuously between selections.
373
+
374
+ ```tsx
375
+ <Tabs defaultValue="overview">
376
+ <Tabs.List aria-label="Project views">
377
+ <Tabs.Tab id="overview">Overview</Tabs.Tab>
378
+ <Tabs.Tab id="activity">Activity</Tabs.Tab>
379
+ </Tabs.List>
380
+ <Tabs.Panel id="overview">Project overview</Tabs.Panel>
381
+ <Tabs.Panel id="activity">Recent activity</Tabs.Panel>
382
+ </Tabs>
383
+ ```
384
+
385
+ Each `Tabs.Tab` identity must match one `Tabs.Panel`. Use `Tabs.Panels` when
386
+ the panels come from a dynamic collection; static panels may remain direct
387
+ children of `Tabs`.
388
+
389
+ ## Table
390
+
391
+ `Table` presents structured rows and columns with accessible navigation. Row
392
+ selection uses the same string-value contract as ListBox, while sorting reports
393
+ the requested column and direction without taking ownership of the data order.
394
+
395
+ ```tsx
396
+ <Table
397
+ aria-label="Processes"
398
+ selectionMode="multiple"
399
+ value={selectedProcesses}
400
+ onChange={setSelectedProcesses}
401
+ sort={sort}
402
+ onSortChange={setSort}
403
+ >
404
+ <Table.Header>
405
+ <Table.Column id="name" isRowHeader allowsSorting>Name</Table.Column>
406
+ <Table.Column id="state" allowsSorting>State</Table.Column>
407
+ </Table.Header>
408
+ <Table.Body>
409
+ <Table.Row id="editor">
410
+ <Table.Cell>Editor</Table.Cell>
411
+ <Table.Cell>Running</Table.Cell>
412
+ </Table.Row>
413
+ </Table.Body>
414
+ </Table>
415
+ ```
416
+
417
+ Selection is disabled by default. Set `selectionMode` to `"single"` or
418
+ `"multiple"` to enable it. Sortable columns emit `{ column, direction }`
419
+ through `onSortChange`; reorder the rows in application state and pass the
420
+ descriptor back through `sort`. `Table.Body` supports static rows, dynamic
421
+ `items`, and `renderEmptyState`.
422
+
423
+ Table owns tabular behavior and row presentation without adding a material
424
+ container or scrolling boundary. Compose Surface and ScrollArea around it when
425
+ the surrounding interface needs either one.
426
+
427
+ ## ProgressBar
428
+
429
+ `ProgressBar` communicates the progress of an operation without becoming an
430
+ input. Determinate progress exposes its current value and percentage;
431
+ indeterminate progress omits the current value and presents ongoing activity.
432
+
433
+ ```tsx
434
+ <ProgressBar label="Uploading" value={64} />
435
+ <ProgressBar aria-label="Connecting" indeterminate />
436
+ ```
437
+
438
+ The default range is `0` through `100`. `formatOptions` controls the generated
439
+ value text, while `valueLabel` supplies an explicit visible and accessible
440
+ value. Color, size, typography, and motion follow the active React UI contracts.
441
+
284
442
  ## Development
285
443
 
286
444
  ```sh
@@ -0,0 +1,45 @@
1
+ import type { CSSProperties, ReactElement, ReactNode } from "react";
2
+ import type { DisclosureGroupProps as AriaDisclosureGroupProps } from "react-aria-components";
3
+ import type { ControlColor } from "./control.js";
4
+ import { DisclosureContent, DisclosureTrigger, type DisclosureRootProps } from "./disclosure.js";
5
+ import { type LayoutGap } from "./layout.js";
6
+ import type { RadiusProps } from "./radius.js";
7
+ import type { ScaleLevel } from "./scale.js";
8
+ type AccordionRootBaseProps = Omit<AriaDisclosureGroupProps, "allowsMultipleExpanded" | "className" | "defaultExpandedKeys" | "expandedKeys" | "isDisabled" | "onExpandedChange" | "style"> & RadiusProps & Readonly<{
9
+ children?: ReactNode;
10
+ className?: string;
11
+ color?: ControlColor;
12
+ disabled?: boolean;
13
+ gap?: LayoutGap;
14
+ size?: ScaleLevel;
15
+ style?: CSSProperties;
16
+ }>;
17
+ export type AccordionSingleExpansionProps = Readonly<{
18
+ multiple?: false;
19
+ value?: string | null;
20
+ defaultValue?: string | null;
21
+ onChange?: (value: string | null) => void;
22
+ }>;
23
+ export type AccordionMultipleExpansionProps = Readonly<{
24
+ multiple: true;
25
+ value?: readonly string[];
26
+ defaultValue?: readonly string[];
27
+ onChange?: (value: readonly string[]) => void;
28
+ }>;
29
+ export type AccordionRootProps = AccordionRootBaseProps & (AccordionSingleExpansionProps | AccordionMultipleExpansionProps);
30
+ /** A coordinated group of identified Disclosure items. */
31
+ export declare const AccordionRoot: import("react").ForwardRefExoticComponent<AccordionRootProps & import("react").RefAttributes<HTMLDivElement>>;
32
+ export interface AccordionItemProps extends Omit<DisclosureRootProps, "defaultExpanded" | "expanded" | "onChange"> {
33
+ readonly id: string;
34
+ }
35
+ /** One identified Disclosure governed by its nearest Accordion. */
36
+ export declare const AccordionItem: import("react").ForwardRefExoticComponent<AccordionItemProps & import("react").RefAttributes<HTMLDivElement>>;
37
+ interface AccordionComponent {
38
+ (properties: AccordionRootProps): ReactElement | null;
39
+ readonly Item: typeof AccordionItem;
40
+ readonly Trigger: typeof DisclosureTrigger;
41
+ readonly Content: typeof DisclosureContent;
42
+ }
43
+ export declare const Accordion: AccordionComponent;
44
+ export type AccordionProps = AccordionRootProps;
45
+ export {};
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, forwardRef, useContext, useMemo } from "react";
3
+ import { DisclosureGroup as AriaDisclosureGroup } from "react-aria-components";
4
+ import { DisclosureContent, DisclosureRoot, DisclosureTrigger } from "./disclosure.js";
5
+ import { resolveGap } from "./layout.js";
6
+ import { stringKeys } from "./selection.js";
7
+ import { useAppearance } from "./ui-provider.js";
8
+ const AccordionThemeContext = createContext(null);
9
+ /** A coordinated group of identified Disclosure items. */
10
+ export const AccordionRoot = forwardRef(function AccordionRoot(properties, ref) {
11
+ const { children, className, color, defaultValue, disabled = false, gap = "xsmall", multiple = false, onChange, radius = "medium", size = "medium", style, value, ...native } = properties;
12
+ const appearance = useAppearance();
13
+ const context = useMemo(() => ({ color, radius, size }), [color, radius, size]);
14
+ return _jsx(AccordionThemeContext.Provider, { value: context, children: _jsx(AriaDisclosureGroup, { ...native, ...expansionProperties(properties), ref: ref, allowsMultipleExpanded: multiple, className: className, isDisabled: disabled, style: {
15
+ display: "grid",
16
+ gap: resolveGap(gap, appearance),
17
+ width: "100%",
18
+ minWidth: 0,
19
+ ...style
20
+ }, children: children }) });
21
+ });
22
+ function expansionProperties(properties) {
23
+ const expandedKeys = expansionKeys(properties.value);
24
+ const defaultExpandedKeys = expansionKeys(properties.defaultValue);
25
+ const onExpandedChange = properties.onChange == null
26
+ ? undefined
27
+ : (keys) => {
28
+ const values = stringKeys(keys);
29
+ if (properties.multiple)
30
+ properties.onChange?.(values);
31
+ else
32
+ properties.onChange?.(values[0] ?? null);
33
+ };
34
+ return {
35
+ ...(expandedKeys === undefined ? {} : { expandedKeys }),
36
+ ...(defaultExpandedKeys === undefined ? {} : { defaultExpandedKeys }),
37
+ ...(onExpandedChange === undefined ? {} : { onExpandedChange })
38
+ };
39
+ }
40
+ function expansionKeys(value) {
41
+ if (value === undefined)
42
+ return undefined;
43
+ if (value === null)
44
+ return new Set();
45
+ return new Set(typeof value === "string" ? [value] : value);
46
+ }
47
+ /** One identified Disclosure governed by its nearest Accordion. */
48
+ export const AccordionItem = forwardRef(function AccordionItem({ color, radius, size, ...properties }, ref) {
49
+ const inherited = useAccordionTheme();
50
+ return _jsx(DisclosureRoot, { ...properties, ref: ref, color: color ?? inherited.color, radius: radius ?? inherited.radius, size: size ?? inherited.size });
51
+ });
52
+ function useAccordionTheme() {
53
+ const context = useContext(AccordionThemeContext);
54
+ if (context == null)
55
+ throw new Error("Accordion.Item must be used inside Accordion");
56
+ return context;
57
+ }
58
+ export const Accordion = Object.assign(AccordionRoot, {
59
+ Item: AccordionItem,
60
+ Trigger: DisclosureTrigger,
61
+ Content: DisclosureContent
62
+ });
@@ -9,7 +9,6 @@ export type AlertDialogContentProps = Omit<DialogContentProps, "role">;
9
9
  export declare const AlertDialogContent: import("react").ForwardRefExoticComponent<AlertDialogContentProps & import("react").RefAttributes<HTMLElement>>;
10
10
  /** A dialog requiring an explicit decision rather than ambient dismissal. */
11
11
  export declare const AlertDialog: typeof AlertDialogRoot & {
12
- Root: typeof AlertDialogRoot;
13
12
  Trigger: import("react").ForwardRefExoticComponent<import("./button.js").ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
14
13
  Backdrop: import("react").ForwardRefExoticComponent<DialogBackdropProps & import("react").RefAttributes<HTMLDivElement>>;
15
14
  Content: import("react").ForwardRefExoticComponent<AlertDialogContentProps & import("react").RefAttributes<HTMLElement>>;
@@ -12,7 +12,6 @@ export const AlertDialogContent = forwardRef(function AlertDialogContent(propert
12
12
  });
13
13
  /** A dialog requiring an explicit decision rather than ambient dismissal. */
14
14
  export const AlertDialog = Object.assign(AlertDialogRoot, {
15
- Root: AlertDialogRoot,
16
15
  Trigger: DialogTrigger,
17
16
  Backdrop: AlertDialogBackdrop,
18
17
  Content: AlertDialogContent,
@@ -0,0 +1,9 @@
1
+ import type { KeyboardEventHandler, ReactNode } from "react";
2
+ import type { Direction } from "./direction.js";
3
+ /** Aligns React Aria's directional keyboard behavior with the React UI direction contract. */
4
+ export declare function useAriaDirectionBridge(direction: Direction): KeyboardEventHandler<HTMLElement> | undefined;
5
+ /** Owns the non-layout DOM boundary needed to intercept React Aria keyboard behavior. */
6
+ export declare function AriaDirectionBoundary({ children, direction }: Readonly<{
7
+ children: ReactNode;
8
+ direction: Direction;
9
+ }>): import("react").JSX.Element;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { useLocale } from "react-aria/I18nProvider";
4
+ const ariaDirectionBoundary = "data-phreshos-ui-aria-direction";
5
+ const redirectedEvents = new WeakSet();
6
+ /** Aligns React Aria's directional keyboard behavior with the React UI direction contract. */
7
+ export function useAriaDirectionBridge(direction) {
8
+ const { direction: ariaDirection } = useLocale();
9
+ return useMemo(() => {
10
+ if (direction === ariaDirection)
11
+ return undefined;
12
+ return event => {
13
+ if (redirectedEvents.has(event.nativeEvent))
14
+ return;
15
+ if (event.defaultPrevented || (event.key !== "ArrowLeft" && event.key !== "ArrowRight"))
16
+ return;
17
+ const target = event.target;
18
+ if (!(target instanceof Element) || target.closest(`[${ariaDirectionBoundary}]`) !== event.currentTarget)
19
+ return;
20
+ const editable = target.closest("[contenteditable=true]");
21
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || (editable && editable.getAttribute("role") !== "spinbutton"))
22
+ return;
23
+ event.preventDefault();
24
+ event.stopPropagation();
25
+ // React Aria currently derives keyboard direction from locale. Redirect only
26
+ // the directional key so language and every other locale behavior stay intact.
27
+ const redirected = new KeyboardEvent(event.nativeEvent.type, {
28
+ key: event.key === "ArrowLeft" ? "ArrowRight" : "ArrowLeft",
29
+ code: event.code,
30
+ location: event.location,
31
+ ctrlKey: event.ctrlKey,
32
+ shiftKey: event.shiftKey,
33
+ altKey: event.altKey,
34
+ metaKey: event.metaKey,
35
+ repeat: event.repeat,
36
+ bubbles: true,
37
+ cancelable: true,
38
+ composed: true
39
+ });
40
+ redirectedEvents.add(redirected);
41
+ target.dispatchEvent(redirected);
42
+ };
43
+ }, [ariaDirection, direction]);
44
+ }
45
+ /** Owns the non-layout DOM boundary needed to intercept React Aria keyboard behavior. */
46
+ export function AriaDirectionBoundary({ children, direction }) {
47
+ const onKeyDownCapture = useAriaDirectionBridge(direction);
48
+ return _jsx("div", { dir: direction, "data-phreshos-ui-aria-direction": "", onKeyDownCapture: onKeyDownCapture, style: { display: "contents" }, children: children });
49
+ }
@@ -0,0 +1,8 @@
1
+ import type { CSSProperties } from "react";
2
+ import { type ControlProps, type ControlTheme } from "./control.js";
3
+ /** Shared month layout used by both single-date and range calendar roots. */
4
+ export declare function CalendarLayout({ color, range, size, theme }: Pick<ControlProps, "color" | "size"> & Readonly<{
5
+ range?: boolean;
6
+ theme: ControlTheme;
7
+ }>): import("react").JSX.Element;
8
+ export declare function calendarRootStyle(theme: ControlTheme, disabled?: boolean, style?: CSSProperties): CSSProperties;
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarHeading } from "react-aria-components";
3
+ import { Button } from "./button.js";
4
+ import { resolveColorLevel, solidColors } from "./color.js";
5
+ import { controlFontWeight, controlOpacity } from "./control.js";
6
+ import { useDirection } from "./direction.js";
7
+ /** Shared month layout used by both single-date and range calendar roots. */
8
+ export function CalendarLayout({ color, range = false, size, theme }) {
9
+ const direction = useDirection();
10
+ return _jsxs(_Fragment, { children: [_jsxs("header", { style: { display: "grid", gridTemplateColumns: "auto 1fr auto", alignItems: "center", gap: theme.gap }, children: [_jsx(Button, { slot: "previous", color: color, size: size, "aria-label": "Previous month", style: navigationStyle(theme), children: _jsx(Chevron, { direction: direction === "rtl" ? "next" : "previous" }) }), _jsx(CalendarHeading, { style: {
11
+ margin: 0,
12
+ textAlign: "center",
13
+ fontFamily: "inherit",
14
+ fontSize: "inherit",
15
+ fontStyle: "inherit",
16
+ lineHeight: "inherit",
17
+ fontWeight: controlFontWeight
18
+ } }), _jsx(Button, { slot: "next", color: color, size: size, "aria-label": "Next month", style: navigationStyle(theme), children: _jsx(Chevron, { direction: direction === "rtl" ? "previous" : "next" }) })] }), _jsxs(CalendarGrid, { style: {
19
+ width: "max-content",
20
+ margin: 0,
21
+ tableLayout: "fixed",
22
+ border: 0,
23
+ borderRadius: 0,
24
+ borderCollapse: "separate",
25
+ borderSpacing: Math.max(1, theme.gap / 4),
26
+ background: "transparent"
27
+ }, children: [_jsx(CalendarGridHeader, { children: day => _jsx(CalendarHeaderCell, { style: {
28
+ height: theme.height * 0.72,
29
+ padding: 0,
30
+ border: 0,
31
+ background: "transparent",
32
+ textAlign: "center",
33
+ fontFamily: "inherit",
34
+ fontSize: "inherit",
35
+ fontStyle: "inherit",
36
+ lineHeight: "inherit",
37
+ fontWeight: controlFontWeight,
38
+ opacity: controlOpacity.secondary
39
+ }, children: day }) }), _jsx(CalendarGridBody, { children: date => _jsx(CalendarCell, { date: date, style: state => calendarCellStyle(theme, state, range) }) })] })] });
40
+ }
41
+ export function calendarRootStyle(theme, disabled = false, style) {
42
+ return {
43
+ ...style,
44
+ display: "grid",
45
+ width: "max-content",
46
+ maxWidth: "100%",
47
+ gap: theme.gap,
48
+ padding: theme.spacing,
49
+ fontFamily: "inherit",
50
+ fontSize: theme.fontSize,
51
+ lineHeight: 1.5,
52
+ opacity: disabled ? controlOpacity.disabled : 1
53
+ };
54
+ }
55
+ function navigationStyle(theme) {
56
+ return {
57
+ width: theme.height,
58
+ paddingInline: 0
59
+ };
60
+ }
61
+ function calendarCellStyle(theme, state, range) {
62
+ const rangeInterior = range && state.isSelected && !state.isSelectionStart && !state.isSelectionEnd
63
+ ? rangeInteriorPaint(theme)
64
+ : null;
65
+ const paint = state.isSelected
66
+ ? rangeInterior ?? theme.paints.palette.pressed
67
+ : state.isPressed
68
+ ? theme.paints.subtle.pressed
69
+ : state.isHovered || state.isFocused
70
+ ? theme.paints.subtle.hover
71
+ : null;
72
+ return {
73
+ ...theme.transition,
74
+ display: "grid",
75
+ placeItems: "center",
76
+ boxSizing: "border-box",
77
+ width: theme.height,
78
+ height: theme.height,
79
+ borderRadius: theme.radius,
80
+ outline: state.isFocusVisible ? `1px solid ${theme.focusColor}` : "none",
81
+ outlineOffset: -1,
82
+ background: paint?.background ?? "transparent",
83
+ color: paint?.color ?? "inherit",
84
+ opacity: state.isDisabled || state.isUnavailable
85
+ ? controlOpacity.disabled
86
+ : state.isOutsideMonth ? controlOpacity.secondary : 1,
87
+ textDecoration: state.isUnavailable ? "line-through" : "none",
88
+ cursor: state.isDisabled || state.isUnavailable ? "not-allowed" : "pointer",
89
+ fontVariantNumeric: "tabular-nums"
90
+ };
91
+ }
92
+ function rangeInteriorPaint(theme) {
93
+ // Range interiors are the same Appearance color at its defined lighter
94
+ // treatment; endpoints retain the stronger selected control paint.
95
+ const background = resolveColorLevel(theme.tint, "soft");
96
+ return {
97
+ background,
98
+ color: solidColors(background, theme.background, theme.foreground).rest.color
99
+ };
100
+ }
101
+ function Chevron({ direction }) {
102
+ return _jsx("svg", { "aria-hidden": "true", width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: _jsx("path", { d: direction === "previous" ? "m7.5 2-4 4 4 4" : "m4.5 2 4 4-4 4" }) });
103
+ }