@phreshos/react-ui 0.1.30 → 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 (80) hide show
  1. package/README.md +162 -18
  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 +25 -9
  43. package/dist/main.js +25 -9
  44. package/dist/material-paint.js +19 -1
  45. package/dist/menu.d.ts +22 -4
  46. package/dist/menu.js +25 -6
  47. package/dist/panel.d.ts +0 -1
  48. package/dist/panel.js +0 -1
  49. package/dist/popover.d.ts +0 -1
  50. package/dist/popover.js +13 -3
  51. package/dist/progress-bar.d.ts +22 -0
  52. package/dist/progress-bar.js +46 -0
  53. package/dist/radio.d.ts +5 -5
  54. package/dist/radio.js +8 -4
  55. package/dist/radius.js +1 -1
  56. package/dist/range-calendar.d.ts +19 -0
  57. package/dist/range-calendar.js +15 -0
  58. package/dist/scroll-area.d.ts +4 -1
  59. package/dist/scroll-area.js +3 -3
  60. package/dist/select.js +4 -14
  61. package/dist/selection.d.ts +18 -0
  62. package/dist/selection.js +28 -0
  63. package/dist/slider.js +1 -1
  64. package/dist/spacing.js +1 -1
  65. package/dist/table.d.ts +78 -0
  66. package/dist/table.js +148 -0
  67. package/dist/tabs.d.ts +45 -0
  68. package/dist/tabs.js +103 -0
  69. package/dist/time-field.d.ts +22 -0
  70. package/dist/time-field.js +13 -0
  71. package/dist/toggle-indicator.js +1 -1
  72. package/dist/toolbar.d.ts +46 -0
  73. package/dist/toolbar.js +121 -0
  74. package/dist/tooltip.d.ts +0 -1
  75. package/dist/tooltip.js +0 -1
  76. package/dist/tree.d.ts +56 -0
  77. package/dist/tree.js +134 -0
  78. package/dist/window.d.ts +6 -3
  79. package/dist/window.js +10 -7
  80. package/package.json +3 -1
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;
@@ -135,8 +141,8 @@ branch, while `false` removes the shadow. Each option accepts a scale level or
135
141
  direct number. The shadow is a neutral black
136
142
  outer shadow. Surface-based controls expose the same `shadow` prop.
137
143
  For text fields and Select, `material` targets the field or trigger; for Checkbox,
138
- Switch, and Radio, it targets the indicator. RadioGroup supplies material defaults
139
- 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
140
146
  inheritance path.
141
147
 
142
148
  ```tsx
@@ -149,6 +155,11 @@ inheritance path.
149
155
  `Surface`. Both materials retain Surface defaults; the content inset follows
150
156
  Appearance spacing. Positioning, modality, and lifecycle belong to the caller.
151
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
+
152
163
  ```tsx
153
164
  import { Panel } from "@phreshos/react-ui"
154
165
 
@@ -207,6 +218,9 @@ semantic content remains explicit.
207
218
 
208
219
  `Popover` presents anchored non-modal content. `DropdownMenu` and
209
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`.
210
224
  `Dialog` blocks interaction behind it and may be dismissable, while
211
225
  `AlertDialog` requires an explicit decision by default. `Tooltip` supplies a
212
226
  short description on focus or hover. Portal cleanup, focus restoration,
@@ -219,8 +233,8 @@ Every input uses Appearance colors and the same five `size` levels as Button.
219
233
  `color` selects the fill independently from material. Input, Textarea, Select,
220
234
  and Button use `default:base` when it is omitted. Invalid
221
235
  fields use danger instead. Text fields share Surface's glass edge. Interaction shades
222
- derive from the base color's lightness, and text or selection marks use whichever
223
- 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.
224
238
  That text or mark color stays unchanged across interaction shades.
225
239
  These are component-owned derivations, not additional Appearance settings.
226
240
  Theme names never imply particular colors. No component requires a Client or
@@ -245,10 +259,19 @@ behavior are never delayed by visual animation.
245
259
  | --- | --- | --- |
246
260
  | `Input` | `value`, `defaultValue`, `onChange(string)` | Single-line text; supports text, email, password, search, URL, and telephone types |
247
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 |
248
268
  | `Checkbox` | `checked`, `defaultChecked`, `onChange(boolean)` | Independent selection; `indeterminate` represents a mixed state |
249
269
  | `Switch` | `checked`, `defaultChecked`, `onChange(boolean)` | An on/off setting |
250
- | `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 |
251
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 |
252
275
  | `Slider` | `value`, `defaultValue`, `onChange(number)` | One numeric value; `minValue`, `maxValue`, `step`, and `onChangeEnd`; horizontal by default |
253
276
 
254
277
  Use `label` for visible labels, or `aria-label` / `aria-labelledby` for an
@@ -257,44 +280,165 @@ accessible name without visible text. `description` provides associated help.
257
280
  `name` participates in native form submission. Controlled values remain owned
258
281
  by the caller; omit them and use defaults for internal state and form reset.
259
282
 
260
- Text fields, Checkbox, Switch, and RadioGroup also support `readOnly`: the
283
+ Text fields, ComboBox, Checkbox, Switch, and RadioGroup also support `readOnly`: the
261
284
  value cannot change, but the control remains focusable. These fields and
262
285
  Select support `required`, `invalid`, `errorMessage`, and React Aria's native
263
- 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
264
287
  group. Slider represents a bounded number rather than a required text or
265
288
  choice field, and has no read-only or validation-error mode. Select has no
266
289
  read-only mode; disable it when selection must be unavailable.
267
290
 
268
- `Input`, `Textarea`, and `Select` accept `radius`. Other inputs retain their
269
- 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;
270
294
  default visual properties remain component-owned. Input and Textarea refs
271
295
  target their native text controls; other refs target the root div. Checkbox,
272
- Switch, and Radio additionally accept `inputRef` for their native input.
296
+ Switch, and RadioGroup.Item additionally accept `inputRef` for their native input.
273
297
 
274
298
  ```tsx
275
- 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"
276
301
 
277
302
  <Input label="Name" name="name" required />
278
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") }} />
279
310
  <Checkbox label="Remember this choice" name="remember" />
280
311
  <Switch label="Notifications" name="notifications" defaultChecked />
281
312
  <RadioGroup label="Layout" name="layout" defaultValue="grid">
282
- <Radio label="Grid" value="grid" />
283
- <Radio label="List" value="list" />
313
+ <RadioGroup.Item label="Grid" value="grid" />
314
+ <RadioGroup.Item label="List" value="list" />
284
315
  </RadioGroup>
285
316
  <Select label="Sort" name="sort" options={[
286
317
  { value: "name", label: "Name" },
287
318
  { value: "date", label: "Date" }
288
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>
289
339
  <Slider label="Volume" name="volume" defaultValue={50} minValue={0} maxValue={100} step={1} />
290
340
  ```
291
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
+
292
362
  Each control delegates focus, keyboard, form, and selection behavior to its
293
363
  accessible behavioral primitive rather than recreating those systems. Select's
294
- 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
295
365
  animation preferences. The preview Program demonstrates each input's
296
366
  sizes, colors, state, and interaction without overriding its visual defaults.
297
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
+
298
442
  ## Development
299
443
 
300
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
+ }
@@ -0,0 +1,5 @@
1
+ import type { ControlTheme } from "./control.js";
2
+ /** Calendar trigger shared by date picker fields. */
3
+ export declare function CalendarTrigger({ theme }: Readonly<{
4
+ theme: ControlTheme;
5
+ }>): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button as AriaButton } from "react-aria-components";
3
+ /** Calendar trigger shared by date picker fields. */
4
+ export function CalendarTrigger({ theme }) {
5
+ const inset = Math.max(8, theme.spacing) - Math.max(4, theme.spacing / 2);
6
+ return _jsx(AriaButton, { "data-date-control-end": "", style: state => ({
7
+ ...theme.transition,
8
+ appearance: "none",
9
+ display: "grid",
10
+ placeItems: "center",
11
+ alignSelf: "stretch",
12
+ flex: "0 0 auto",
13
+ width: theme.height - Math.max(4, theme.spacing / 2),
14
+ height: "auto",
15
+ marginBlock: inset,
16
+ marginInlineEnd: -Math.max(4, theme.spacing / 2),
17
+ padding: 0,
18
+ border: 0,
19
+ borderRadius: Math.max(2, theme.spacing / 3),
20
+ outline: state.isFocusVisible ? `1px solid ${theme.focusColor}` : "none",
21
+ background: state.isPressed
22
+ ? theme.paints.subtle.pressed.background
23
+ : state.isHovered ? theme.paints.subtle.hover.background : "transparent",
24
+ color: "inherit",
25
+ cursor: state.isDisabled ? "not-allowed" : "pointer"
26
+ }), children: _jsx(CalendarIcon, {}) });
27
+ }
28
+ function CalendarIcon() {
29
+ return _jsxs("svg", { "aria-hidden": "true", width: "15", height: "15", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.35", children: [_jsx("rect", { x: "2.25", y: "3.25", width: "11.5", height: "10.5", rx: "2" }), _jsx("path", { d: "M5 1.75v3M11 1.75v3M2.5 6.5h11" })] });
30
+ }
@@ -0,0 +1,18 @@
1
+ import type { CalendarDate } from "@internationalized/date";
2
+ import type { CalendarProps as AriaCalendarProps } from "react-aria-components";
3
+ import { type ControlOverrides, type ControlProps } from "./control.js";
4
+ import type { RadiusProps } from "./radius.js";
5
+ type CalendarOverrides = ControlOverrides | "children" | "isReadOnly" | "isInvalid" | "value" | "defaultValue" | "onChange" | "minValue" | "maxValue" | "isDateUnavailable" | "selectionMode" | "visibleDuration" | "createCalendar" | "errorMessage" | "validationState";
6
+ export interface CalendarProps extends Omit<AriaCalendarProps<CalendarDate>, CalendarOverrides>, ControlProps, RadiusProps {
7
+ readonly value?: CalendarDate | null;
8
+ readonly defaultValue?: CalendarDate | null;
9
+ readonly onChange?: (value: CalendarDate) => void;
10
+ readonly minValue?: CalendarDate | null;
11
+ readonly maxValue?: CalendarDate | null;
12
+ readonly isDateUnavailable?: (date: CalendarDate) => boolean;
13
+ readonly readOnly?: boolean;
14
+ readonly invalid?: boolean;
15
+ }
16
+ /** A locale-aware, keyboard-navigable month view for selecting one date. */
17
+ export declare const Calendar: import("react").ForwardRefExoticComponent<CalendarProps & import("react").RefAttributes<HTMLDivElement>>;
18
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef } from "react";
3
+ import { Calendar as AriaCalendar } from "react-aria-components";
4
+ import { AriaDirectionBoundary } from "./aria-direction.js";
5
+ import { CalendarLayout, calendarRootStyle } from "./calendar-layout.js";
6
+ import { useControlTheme } from "./control.js";
7
+ import { resolveDirection, useDirection } from "./direction.js";
8
+ /** A locale-aware, keyboard-navigable month view for selecting one date. */
9
+ export const Calendar = forwardRef(function Calendar({ disabled, readOnly, invalid, size, color, radius, style, className, isDateUnavailable, ...properties }, ref) {
10
+ const theme = useControlTheme({ size, color, radius });
11
+ const direction = resolveDirection(properties.dir, useDirection());
12
+ return _jsx(AriaDirectionBoundary, { direction: direction, children: _jsx(AriaCalendar, { ...properties, ref: ref, dir: direction, className: className, isDisabled: disabled, isReadOnly: readOnly, isInvalid: invalid, isDateUnavailable: isDateUnavailable == null ? undefined : date => isDateUnavailable(date), style: calendarRootStyle(theme, disabled, style), children: _jsx(CalendarLayout, { color: color, size: size, theme: theme }) }) });
13
+ });