@rio-m3e/core 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +312 -18
  2. package/dist/index.js +625 -183
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -1,3 +1,36 @@
1
+ /**
2
+ * The attributes of one part of a component's markup, in HTML form: names as HTML spells them
3
+ * (`class`, `aria-pressed`, `data-size`) and string values, with `undefined` leaving an attribute
4
+ * out. Each component's `parts` returns one of these per part. See "Core output" in
5
+ * `docs/conventions.md`.
6
+ */
7
+ export type PartAttrs = Record<string, string | undefined>;
8
+ /**
9
+ * One node of a component's skeleton: the markup an adapter renders, part by part. Adapters
10
+ * render it their own way; contract tests check what they rendered against it. Each component
11
+ * exports its skeleton (`buttonSkeleton`, `switchSkeleton`, ...).
12
+ *
13
+ * `Part` narrows the names to the component's parts, so a typo fails the build. `When` lists the
14
+ * decisions the skeleton may use; leave it out when every part always renders. Skeletons are
15
+ * shared objects, so everything is read-only.
16
+ */
17
+ export interface PartNode<Part extends string = string, When extends string = never> {
18
+ /** The part, named as in the component's `parts`. */
19
+ readonly part: Part;
20
+ /** The element the part is, as `tagName` in lower case. */
21
+ readonly tag: keyof HTMLElementTagNameMap;
22
+ /**
23
+ * The component API's decision that says whether the part renders, such as `'showsIcon'`.
24
+ * Without it, the part always renders.
25
+ */
26
+ readonly when?: When;
27
+ /**
28
+ * Content from the app (slots, children) goes inside. The content itself isn't checked against
29
+ * the skeleton; where a component picks between contents, its contract test checks the pick.
30
+ */
31
+ readonly content?: true;
32
+ readonly children?: readonly PartNode<Part, When>[];
33
+ }
1
34
  export declare const BUTTON_VARIANTS: readonly ["filled", "tonal", "outlined", "elevated", "text"];
2
35
  export type ButtonVariant = (typeof BUTTON_VARIANTS)[number];
3
36
  /** M3E button sizes. `small` is the default. */
@@ -28,6 +61,11 @@ export interface ButtonProps {
28
61
  softDisabled?: boolean;
29
62
  /** Defaults to `'button'`, so a button inside a form doesn't submit it by accident. */
30
63
  type?: 'button' | 'submit' | 'reset';
64
+ /**
65
+ * Identifies the button inside a button group that keeps a selection. Such a group makes the
66
+ * button a toggle and decides whether it is selected.
67
+ */
68
+ value?: ButtonGroupValue;
31
69
  }
32
70
  export declare const buttonDefaults: {
33
71
  variant: "filled";
@@ -39,20 +77,121 @@ export declare const buttonDefaults: {
39
77
  softDisabled: false;
40
78
  type: "button";
41
79
  };
80
+ /** What the adapter has to put in the button, which only the adapter can see. */
81
+ export interface ButtonContent {
82
+ /** Whether there is a leading icon. */
83
+ icon: boolean;
84
+ }
42
85
  export interface ButtonOptions {
43
86
  /** Returns the current prop values. The adapter owns the state. */
44
87
  props: () => ButtonProps;
88
+ /** Returns what content the adapter has. Without it, the button has no icon. */
89
+ content?: () => ButtonContent;
45
90
  /** Called for a click that isn't blocked. */
46
91
  onClick: (event: MouseEvent) => void;
47
- /** Called after an unblocked click on a toggle button, with the new value. */
48
- onSelectedChange: (selected: boolean) => void;
92
+ /** Called after an unblocked click on a toggle button, with the requested value. */
93
+ onChange: (selected: boolean) => void;
49
94
  }
95
+ /**
96
+ * The button's markup, part by part. `buttonSkeleton` says how the adapter nests them.
97
+ */
98
+ export interface ButtonParts {
99
+ root: () => PartAttrs;
100
+ /** Extends small buttons to the 48px target. */
101
+ touch: () => PartAttrs;
102
+ icon: () => PartAttrs;
103
+ label: () => PartAttrs;
104
+ }
105
+ /** How the adapter renders the button's parts. The icon holds the leading icon. */
106
+ export declare const buttonSkeleton: PartNode<keyof ButtonParts, 'showsIcon'>;
50
107
  export interface ButtonApi {
51
- rootAttrs: () => Record<string, string | undefined>;
108
+ parts: ButtonParts;
109
+ /** Whether to render the icon part. */
110
+ showsIcon: () => boolean;
52
111
  /** Attach to the root element's `click`. */
53
112
  handleClick: (event: MouseEvent) => void;
54
113
  }
55
114
  export declare function createButton(options: ButtonOptions): ButtonApi;
115
+ /**
116
+ * M3E button group variants. A standard group spaces its buttons and lets a pressed button push
117
+ * its neighbors; a connected group joins them with 2px gaps and shared corners, and fills the
118
+ * width it is given.
119
+ */
120
+ export declare const BUTTON_GROUP_VARIANTS: readonly ["standard", "connected"];
121
+ export type ButtonGroupVariant = (typeof BUTTON_GROUP_VARIANTS)[number];
122
+ /** Whether the group's buttons can be selected: none, one at a time, or any number. */
123
+ export declare const BUTTON_GROUP_SELECTIONS: readonly ["none", "single", "multiple"];
124
+ export type ButtonGroupSelection = (typeof BUTTON_GROUP_SELECTIONS)[number];
125
+ /** Identifies a button inside a selecting group, through its `value` prop. */
126
+ export type ButtonGroupValue = string | number;
127
+ /** The selected value for `single` (or `undefined`), or the selected values for `multiple`. */
128
+ export type ButtonGroupSelected = ButtonGroupValue | ButtonGroupValue[] | undefined;
129
+ export interface ButtonGroupProps {
130
+ /** Defaults to `'standard'`. */
131
+ variant?: ButtonGroupVariant;
132
+ /** The size of every button in the group that doesn't set its own. Defaults to `'small'`. */
133
+ size?: ButtonSize;
134
+ /** The shape of every button in the group that doesn't set its own. Defaults to `'round'`. */
135
+ shape?: ButtonShape;
136
+ /**
137
+ * Turns buttons that have a `value` into toggle buttons whose selection the group keeps.
138
+ * Defaults to `'none'`, where every button acts on its own.
139
+ */
140
+ selection?: ButtonGroupSelection;
141
+ /** With a selection, keeps at least one button selected: the last one can't be turned off. */
142
+ required?: boolean;
143
+ /** The selected value (`single`) or values (`multiple`). */
144
+ selected?: ButtonGroupSelected;
145
+ }
146
+ export declare const buttonGroupDefaults: {
147
+ variant: "standard";
148
+ size: "small";
149
+ shape: "round";
150
+ selection: "none";
151
+ required: false;
152
+ };
153
+ export interface ButtonGroupOptions {
154
+ /** Returns the current prop values. The adapter owns the state. */
155
+ props: () => ButtonGroupProps;
156
+ /** Called with the requested selection. Adapters wire it to v-model, a callback prop, and so on. */
157
+ onChange: (selected: ButtonGroupSelected) => void;
158
+ }
159
+ /**
160
+ * The button group's markup. `buttonGroupSkeleton` says how the adapter renders it.
161
+ */
162
+ export interface ButtonGroupParts {
163
+ root: () => PartAttrs;
164
+ }
165
+ /** How the adapter renders the button group: one root, with the buttons as its direct children. */
166
+ export declare const buttonGroupSkeleton: PartNode<keyof ButtonGroupParts>;
167
+ export interface ButtonGroupApi {
168
+ parts: ButtonGroupParts;
169
+ /** The size for buttons that don't set their own, with the default filled in. */
170
+ size: () => ButtonSize;
171
+ /** The shape for buttons that don't set their own, with the default filled in. */
172
+ shape: () => ButtonShape;
173
+ /** The selection mode, with the default filled in. */
174
+ selection: () => ButtonGroupSelection;
175
+ /** Whether the button with this value is selected. */
176
+ isSelected: (value: ButtonGroupValue) => boolean;
177
+ /**
178
+ * Requests turning the button with this value on or off. The group applies its selection rules:
179
+ * a single selection turns the previous button off, and `required` keeps the last one on.
180
+ */
181
+ select: (value: ButtonGroupValue, selected: boolean) => void;
182
+ }
183
+ export declare function createButtonGroup(options: ButtonGroupOptions): ButtonGroupApi;
184
+ export interface ButtonGroupInteractionOptions {
185
+ /**
186
+ * How much wider a pressed button gets, as a share of its width. Each neighbor gives up at most
187
+ * the same share of its own width. Defaults to 0.15, the spec's 15%.
188
+ */
189
+ pressedWidthRatio?: number;
190
+ }
191
+ export interface ButtonGroupHandle {
192
+ detach(): void;
193
+ }
194
+ export declare function attachButtonGroup(root: HTMLElement, options?: ButtonGroupInteractionOptions): ButtonGroupHandle;
56
195
  export declare const ICON_BUTTON_VARIANTS: readonly ["filled", "tonal", "outlined", "standard"];
57
196
  export type IconButtonVariant = (typeof ICON_BUTTON_VARIANTS)[number];
58
197
  /** M3E icon button widths. `default` makes a square (or circle) as wide as it is tall. */
@@ -82,6 +221,8 @@ export interface IconButtonProps {
82
221
  softDisabled?: boolean;
83
222
  /** Defaults to `'button'`, so it doesn't submit a form by accident. */
84
223
  type?: 'button' | 'submit' | 'reset';
224
+ /** Identifies the button inside a button group that keeps a selection, as on the button. */
225
+ value?: ButtonGroupValue;
85
226
  }
86
227
  export declare const iconButtonDefaults: {
87
228
  variant: "filled";
@@ -94,15 +235,43 @@ export declare const iconButtonDefaults: {
94
235
  softDisabled: false;
95
236
  type: "button";
96
237
  };
238
+ /** What the adapter has to put in the icon button, which only the adapter can see. */
239
+ export interface IconButtonContent {
240
+ /** Whether there is an icon for the selected state. */
241
+ selectedIcon: boolean;
242
+ }
97
243
  export interface IconButtonOptions {
98
244
  /** Returns the current prop values. The adapter owns the state. */
99
245
  props: () => IconButtonProps;
246
+ /** Returns what content the adapter has. Without it, there is no selected icon. */
247
+ content?: () => IconButtonContent;
100
248
  /** Called for a click that isn't blocked. */
101
249
  onClick: (event: MouseEvent) => void;
102
- /** Called after an unblocked click on a toggle button, with the new value. */
103
- onSelectedChange: (selected: boolean) => void;
250
+ /** Called after an unblocked click on a toggle button, with the requested value. */
251
+ onChange: (selected: boolean) => void;
252
+ }
253
+ /**
254
+ * The icon button's markup, part by part. `iconButtonSkeleton` says how the adapter nests them.
255
+ */
256
+ export interface IconButtonParts {
257
+ root: () => PartAttrs;
258
+ /** Extends small icon buttons to the 48px target. */
259
+ touch: () => PartAttrs;
260
+ icon: () => PartAttrs;
104
261
  }
105
- export declare function createIconButton(options: IconButtonOptions): ButtonApi;
262
+ /**
263
+ * How the adapter renders the icon button's parts. The icon holds the selected icon when
264
+ * `showsSelectedIcon()`, otherwise the default one.
265
+ */
266
+ export declare const iconButtonSkeleton: PartNode<keyof IconButtonParts>;
267
+ export interface IconButtonApi {
268
+ parts: IconButtonParts;
269
+ /** Whether the icon part shows the selected icon instead of the default one. */
270
+ showsSelectedIcon: () => boolean;
271
+ /** Attach to the root element's `click`. */
272
+ handleClick: (event: MouseEvent) => void;
273
+ }
274
+ export declare function createIconButton(options: IconButtonOptions): IconButtonApi;
106
275
  export interface DragInfo {
107
276
  /** Distance from where the press started, in px. */
108
277
  dx: number;
@@ -147,22 +316,21 @@ export declare function attachRipple(element: HTMLElement, options?: RippleOptio
147
316
  export interface ShapeMorphOptions {
148
317
  /** The motion token whose spring drives the morph. Defaults to `'fast-spatial'`. */
149
318
  token?: string;
150
- /** Attributes whose changes can move the target. Defaults to Rio's state attributes. */
151
- watch?: string[];
152
319
  }
153
320
  export interface ShapeMorphHandle {
154
321
  detach(): void;
155
322
  }
156
323
  export declare function attachShapeMorph(element: HTMLElement, options?: ShapeMorphOptions): ShapeMorphHandle;
157
324
  export interface SpringPropertyOptions {
158
- /** A registered <length> custom property that holds the target for the current state. */
325
+ /**
326
+ * A registered custom property that holds the target for the current state: a <length>, read
327
+ * in px, or an <angle>, read in deg. The stylesheet has to transition it (see above).
328
+ */
159
329
  target: string;
160
- /** Writes the animated value in px, or clears it with `undefined` at rest. */
330
+ /** Writes the animated value (px or deg), or clears it with `undefined` at rest. */
161
331
  apply: (value: number | undefined) => void;
162
332
  /** The motion token whose spring drives the value. Defaults to `'fast-spatial'`. */
163
333
  token?: string;
164
- /** Attributes whose changes can move the target. Defaults to Rio's state attributes. */
165
- watch?: string[];
166
334
  }
167
335
  export interface SpringPropertyHandle {
168
336
  /** The current animated value. */
@@ -173,8 +341,6 @@ export interface SpringPropertyHandle {
173
341
  release(velocity?: number): void;
174
342
  detach(): void;
175
343
  }
176
- /** The data attributes Rio's components use for state. */
177
- export declare const STATE_ATTRIBUTES: string[];
178
344
  export declare function attachSpringProperty(element: HTMLElement, options: SpringPropertyOptions): SpringPropertyHandle;
179
345
  export interface SpringParams {
180
346
  /** 1 settles without overshoot; lower values bounce, higher values approach more slowly. */
@@ -220,6 +386,11 @@ export interface Spring {
220
386
  }
221
387
  /** A spring that animates a single number with `requestAnimationFrame`. */
222
388
  export declare function createSpring(options: SpringOptions): Spring;
389
+ /**
390
+ * Whether the user asked for reduced motion. Spatial springs should jump straight to their target
391
+ * (or be skipped) when it is on; see `docs/foundations.md`, section 3.
392
+ */
393
+ export declare function prefersReducedMotion(): boolean;
223
394
  /**
224
395
  * Reads a motion token's spring from CSS variables on an element, for example
225
396
  * `--rio-motion-fast-spatial-damping` and `-stiffness`. Reading them from the element, rather than
@@ -227,11 +398,104 @@ export declare function createSpring(options: SpringOptions): Spring;
227
398
  * Returns `undefined` when the theme doesn't define them.
228
399
  */
229
400
  export declare function readSpringParams(element: Element, token: string): SpringParams | undefined;
401
+ export interface SplitButtonMenuIconHandle {
402
+ detach(): void;
403
+ }
404
+ /** Attach to the trailing button. */
405
+ export declare function attachSplitButtonMenuIcon(trailing: HTMLElement): SplitButtonMenuIconHandle;
406
+ /** Split button colors: the button's, without text. */
407
+ export declare const SPLIT_BUTTON_VARIANTS: readonly ["filled", "tonal", "outlined", "elevated"];
408
+ export type SplitButtonVariant = (typeof SPLIT_BUTTON_VARIANTS)[number];
409
+ /** What the trailing button opens, for `aria-haspopup`. */
410
+ export type SplitButtonPopup = 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';
411
+ export interface SplitButtonProps {
412
+ /**
413
+ * Names the trailing button. It should say that more choices for the leading action are there,
414
+ * such as "More watch options" next to "Watch later".
415
+ */
416
+ menuLabel: string;
417
+ /**
418
+ * Names the leading button when it has no label text, only an icon. Ignored when there is label
419
+ * text, so the visible label stays the name (WCAG 2.5.3).
420
+ */
421
+ label?: string;
422
+ /** Defaults to `'filled'`. */
423
+ variant?: SplitButtonVariant;
424
+ /** Defaults to `'small'`. */
425
+ size?: ButtonSize;
426
+ /** Whether what the trailing button opens is open. */
427
+ open?: boolean;
428
+ /** What the trailing button opens. Defaults to `'menu'`. */
429
+ popup?: SplitButtonPopup;
430
+ /** Native disabled, on both buttons: not focusable and not pressable. The spec's behavior. */
431
+ disabled?: boolean;
432
+ /** Disabled but still focusable, on both buttons, as on the button. Deviates from the spec. */
433
+ softDisabled?: boolean;
434
+ /** The leading button's type. Defaults to `'button'`; the trailing button is always `'button'`. */
435
+ type?: 'button' | 'submit' | 'reset';
436
+ }
437
+ export declare const splitButtonDefaults: {
438
+ variant: "filled";
439
+ size: "small";
440
+ open: false;
441
+ popup: "menu";
442
+ disabled: false;
443
+ softDisabled: false;
444
+ type: "button";
445
+ };
446
+ /** What the adapter has to put in the leading button, which only the adapter can see. */
447
+ export interface SplitButtonContent {
448
+ /** Whether there is a leading icon. */
449
+ icon: boolean;
450
+ /** Whether there is label text. */
451
+ label: boolean;
452
+ }
453
+ export interface SplitButtonOptions {
454
+ /** Returns the current prop values. The adapter owns the state. */
455
+ props: () => SplitButtonProps;
456
+ /** Returns what content the adapter has. Without it, the leading button has only a label. */
457
+ content?: () => SplitButtonContent;
458
+ /** Called for a click on the leading button that isn't blocked. */
459
+ onClick: (event: MouseEvent) => void;
460
+ /** Called with the requested `open` when the trailing button is clicked and isn't blocked. */
461
+ onChange: (open: boolean) => void;
462
+ }
463
+ /** The split button's markup, part by part. `splitButtonSkeleton` says how the adapter nests them. */
464
+ export interface SplitButtonParts {
465
+ root: () => PartAttrs;
466
+ /** The main action, a button with an icon, label text, or both. */
467
+ leading: () => PartAttrs;
468
+ /** The button that opens the menu. */
469
+ trailing: () => PartAttrs;
470
+ /** Extends each button to the 48px target. Both buttons have one. */
471
+ touch: () => PartAttrs;
472
+ icon: () => PartAttrs;
473
+ label: () => PartAttrs;
474
+ /** Holds the expand icon, which turns when the menu opens. */
475
+ menuIcon: () => PartAttrs;
476
+ }
477
+ /**
478
+ * How the adapter renders the split button's parts. The icon and the label hold the leading
479
+ * button's content; the menu icon holds the expand icon the app provides.
480
+ */
481
+ export declare const splitButtonSkeleton: PartNode<keyof SplitButtonParts, 'showsIcon' | 'showsLabel'>;
482
+ export interface SplitButtonApi {
483
+ parts: SplitButtonParts;
484
+ /** Whether to render the icon part. */
485
+ showsIcon: () => boolean;
486
+ /** Whether to render the label part. */
487
+ showsLabel: () => boolean;
488
+ /** Attach to the leading button's `click`. */
489
+ handleLeadingClick: (event: MouseEvent) => void;
490
+ /** Attach to the trailing button's `click`. */
491
+ handleTrailingClick: (event: MouseEvent) => void;
492
+ }
493
+ export declare function createSplitButton(options: SplitButtonOptions): SplitButtonApi;
230
494
  export interface SwitchHandleOptions {
231
495
  /** Returns whether the switch is on. */
232
496
  selected: () => boolean;
233
497
  /** Called when a drag ends on the other side. */
234
- onSelectedChange: (selected: boolean) => void;
498
+ onChange: (selected: boolean) => void;
235
499
  }
236
500
  export interface SwitchHandleHandle {
237
501
  detach(): void;
@@ -254,14 +518,44 @@ export declare const switchDefaults: {
254
518
  disabled: false;
255
519
  softDisabled: false;
256
520
  };
521
+ /** What the adapter has to put in the switch, which only the adapter can see. */
522
+ export interface SwitchContent {
523
+ /** Whether there is an icon for the handle while the switch is on. */
524
+ selectedIcon: boolean;
525
+ /** Whether there is an icon for the handle while the switch is off. */
526
+ unselectedIcon: boolean;
527
+ }
257
528
  export interface SwitchOptions {
258
529
  /** Returns the current prop values. The adapter owns the state. */
259
530
  props: () => SwitchProps;
260
- /** Called with the requested value. Adapters wire it to v-model, onChange, and so on. */
261
- onSelectedChange: (selected: boolean) => void;
531
+ /** Returns what content the adapter has. Without it, the handle has no icons. */
532
+ content?: () => SwitchContent;
533
+ /** Called with the requested value. Adapters wire it to v-model, a callback prop, and so on. */
534
+ onChange: (selected: boolean) => void;
262
535
  }
536
+ /**
537
+ * The switch's markup, part by part. `switchSkeleton` says how the adapter nests them.
538
+ */
539
+ export interface SwitchParts {
540
+ root: () => PartAttrs;
541
+ /** Extends the 32px track to the 48px target. */
542
+ touch: () => PartAttrs;
543
+ state: () => PartAttrs;
544
+ handle: () => PartAttrs;
545
+ icon: () => PartAttrs;
546
+ }
547
+ /**
548
+ * How the adapter renders the switch's parts. The state part is the circle where the state layer
549
+ * and the ripple draw. The icon holds the selected icon when `showsSelectedIcon()`, otherwise the
550
+ * unselected one.
551
+ */
552
+ export declare const switchSkeleton: PartNode<keyof SwitchParts, 'showsIcon'>;
263
553
  export interface SwitchApi {
264
- rootAttrs: () => Record<string, string | undefined>;
554
+ parts: SwitchParts;
555
+ /** Whether the handle shows an icon in the current state, which also makes it bigger. */
556
+ showsIcon: () => boolean;
557
+ /** Whether the icon part shows the selected icon: the switch is on and has one. */
558
+ showsSelectedIcon: () => boolean;
265
559
  /** Attach to the root element's `click`. */
266
560
  handleClick: (event: MouseEvent) => void;
267
561
  }
package/dist/index.js CHANGED
@@ -1,3 +1,295 @@
1
+ // src/utils/object.ts
2
+ /**
3
+ * A copy without the properties that are explicitly `undefined`, so spreading it over defaults
4
+ * keeps the defaults: `{ ...defaults, ...omitUndefined(props) }`.
5
+ */
6
+ function omitUndefined(object) {
7
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
8
+ }
9
+ // src/button-group/button-group.ts
10
+ /**
11
+ * M3E button group variants. A standard group spaces its buttons and lets a pressed button push
12
+ * its neighbors; a connected group joins them with 2px gaps and shared corners, and fills the
13
+ * width it is given.
14
+ */
15
+ const BUTTON_GROUP_VARIANTS = ["standard", "connected"];
16
+ /** Whether the group's buttons can be selected: none, one at a time, or any number. */
17
+ const BUTTON_GROUP_SELECTIONS = [
18
+ "none",
19
+ "single",
20
+ "multiple"
21
+ ];
22
+ const buttonGroupDefaults = {
23
+ variant: "standard",
24
+ size: "small",
25
+ shape: "round",
26
+ selection: "none",
27
+ required: false
28
+ };
29
+ /** How the adapter renders the button group: one root, with the buttons as its direct children. */
30
+ const buttonGroupSkeleton = {
31
+ part: "root",
32
+ tag: "div",
33
+ content: true
34
+ };
35
+ const valuesOf = (selected) => selected === void 0 ? [] : Array.isArray(selected) ? selected : [selected];
36
+ function createButtonGroup(options) {
37
+ const props = () => ({
38
+ ...buttonGroupDefaults,
39
+ ...omitUndefined(options.props())
40
+ });
41
+ const isSelected = (value) => {
42
+ const p = props();
43
+ return p.selection !== "none" && valuesOf(p.selected).includes(value);
44
+ };
45
+ return {
46
+ parts: { root() {
47
+ const p = props();
48
+ return {
49
+ class: "rio-button-group",
50
+ role: "group",
51
+ "data-variant": p.variant,
52
+ "data-size": p.size,
53
+ "data-shape": p.shape
54
+ };
55
+ } },
56
+ size: () => props().size,
57
+ shape: () => props().shape,
58
+ selection: () => props().selection,
59
+ isSelected,
60
+ select(value, selected) {
61
+ const p = props();
62
+ if (p.selection === "none" || isSelected(value) === selected) return;
63
+ const current = valuesOf(p.selected);
64
+ if (!selected && p.required && current.length === 1) return;
65
+ if (p.selection === "single") options.onChange(selected ? value : void 0);
66
+ else options.onChange(selected ? [...current, value] : current.filter((other) => other !== value));
67
+ }
68
+ };
69
+ }
70
+ // src/primitives/spring/spring.ts
71
+ /**
72
+ * Where a spring is `t` seconds after it starts at `from` with `velocity`, heading to `to`.
73
+ */
74
+ function solveSpring({ dampingRatio: zeta, stiffness }, from, to, velocity, t) {
75
+ const w0 = Math.sqrt(stiffness);
76
+ const x0 = from - to;
77
+ const v0 = velocity;
78
+ if (Math.abs(zeta - 1) < 1e-6) {
79
+ const b = v0 + w0 * x0;
80
+ const decay = Math.exp(-w0 * t);
81
+ return {
82
+ value: to + (x0 + b * t) * decay,
83
+ velocity: (b - w0 * (x0 + b * t)) * decay
84
+ };
85
+ }
86
+ if (zeta < 1) {
87
+ const a = zeta * w0;
88
+ const wd = w0 * Math.sqrt(1 - zeta * zeta);
89
+ const b = (v0 + a * x0) / wd;
90
+ const decay = Math.exp(-a * t);
91
+ const cos = Math.cos(wd * t);
92
+ const sin = Math.sin(wd * t);
93
+ return {
94
+ value: to + decay * (x0 * cos + b * sin),
95
+ velocity: decay * ((wd * b - a * x0) * cos - (a * b + wd * x0) * sin)
96
+ };
97
+ }
98
+ const root = w0 * Math.sqrt(zeta * zeta - 1);
99
+ const r1 = -zeta * w0 + root;
100
+ const r2 = -zeta * w0 - root;
101
+ const c2 = (v0 - r1 * x0) / (r2 - r1);
102
+ const c1 = x0 - c2;
103
+ const e1 = Math.exp(r1 * t);
104
+ const e2 = Math.exp(r2 * t);
105
+ return {
106
+ value: to + c1 * e1 + c2 * e2,
107
+ velocity: r1 * c1 * e1 + r2 * c2 * e2
108
+ };
109
+ }
110
+ const now = () => performance.now() / 1e3;
111
+ /** A spring that animates a single number with `requestAnimationFrame`. */
112
+ function createSpring(options) {
113
+ const precision = options.precision ?? .01;
114
+ let from = options.value;
115
+ let target = options.value;
116
+ let startVelocity = 0;
117
+ let params;
118
+ let startedAt = 0;
119
+ let frame;
120
+ const state = () => params && frame !== void 0 ? solveSpring(params, from, target, startVelocity, now() - startedAt) : {
121
+ value: target,
122
+ velocity: 0
123
+ };
124
+ const settle = () => {
125
+ if (frame !== void 0) cancelAnimationFrame(frame);
126
+ frame = void 0;
127
+ from = target;
128
+ startVelocity = 0;
129
+ options.onUpdate(target);
130
+ options.onRest?.();
131
+ };
132
+ const tick = () => {
133
+ const { value, velocity } = state();
134
+ if (Math.abs(value - target) < precision && Math.abs(velocity) < precision * 10) {
135
+ settle();
136
+ return;
137
+ }
138
+ options.onUpdate(value);
139
+ frame = requestAnimationFrame(tick);
140
+ };
141
+ return {
142
+ get value() {
143
+ return state().value;
144
+ },
145
+ get velocity() {
146
+ return state().velocity;
147
+ },
148
+ get target() {
149
+ return target;
150
+ },
151
+ get moving() {
152
+ return frame !== void 0;
153
+ },
154
+ to(next, nextParams, velocity) {
155
+ const current = state();
156
+ from = current.value;
157
+ startVelocity = velocity ?? current.velocity;
158
+ target = next;
159
+ params = nextParams;
160
+ startedAt = now();
161
+ if (frame === void 0) frame = requestAnimationFrame(tick);
162
+ },
163
+ jump(value) {
164
+ target = value;
165
+ settle();
166
+ },
167
+ stop() {
168
+ const { value } = state();
169
+ if (frame !== void 0) cancelAnimationFrame(frame);
170
+ frame = void 0;
171
+ from = target = value;
172
+ startVelocity = 0;
173
+ }
174
+ };
175
+ }
176
+ /**
177
+ * Whether the user asked for reduced motion. Spatial springs should jump straight to their target
178
+ * (or be skipped) when it is on; see `docs/foundations.md`, section 3.
179
+ */
180
+ function prefersReducedMotion() {
181
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
182
+ }
183
+ /**
184
+ * Reads a motion token's spring from CSS variables on an element, for example
185
+ * `--rio-motion-fast-spatial-damping` and `-stiffness`. Reading them from the element, rather than
186
+ * from the tokens package, lets a subtree that swaps motion schemes change its springs too.
187
+ * Returns `undefined` when the theme doesn't define them.
188
+ */
189
+ function readSpringParams(element, token) {
190
+ const style = getComputedStyle(element);
191
+ const dampingRatio = parseFloat(style.getPropertyValue(`--rio-motion-${token}-damping`));
192
+ const stiffness = parseFloat(style.getPropertyValue(`--rio-motion-${token}-stiffness`));
193
+ return Number.isFinite(dampingRatio) && Number.isFinite(stiffness) ? {
194
+ dampingRatio,
195
+ stiffness
196
+ } : void 0;
197
+ }
198
+ // src/button-group/interaction.ts
199
+ const PRESSED_WIDTH_RATIO = .15;
200
+ function attachButtonGroup(root, options = {}) {
201
+ const ratio = options.pressedWidthRatio ?? PRESSED_WIDTH_RATIO;
202
+ const items = () => [...root.children].filter((el) => el instanceof HTMLElement);
203
+ const onKeydown = (event) => {
204
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
205
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
206
+ const focusable = items().filter((el) => !el.matches(":disabled") && el.tabIndex >= 0);
207
+ const index = focusable.findIndex((el) => el.contains(document.activeElement));
208
+ if (index === -1) return;
209
+ const next = focusable[index + (event.key === "ArrowRight" !== (getComputedStyle(root).direction === "rtl") ? 1 : -1)];
210
+ if (!next) return;
211
+ event.preventDefault();
212
+ next.focus();
213
+ };
214
+ root.addEventListener("keydown", onKeydown);
215
+ const springs = /* @__PURE__ */ new Map();
216
+ const natural = /* @__PURE__ */ new Map();
217
+ const setWidth = (el, value) => value === void 0 ? el.style.removeProperty("inline-size") : el.style.setProperty("inline-size", `${value}px`);
218
+ const springOf = (el) => {
219
+ let spring = springs.get(el);
220
+ if (!spring) {
221
+ spring = createSpring({
222
+ value: natural.get(el),
223
+ onUpdate: (value) => setWidth(el, value),
224
+ onRest: () => {
225
+ if (springs.get(el)?.target === natural.get(el)) setWidth(el, void 0);
226
+ }
227
+ });
228
+ springs.set(el, spring);
229
+ }
230
+ return spring;
231
+ };
232
+ /**
233
+ * A button's own width, without the width a spring wrote on it. The inline width comes off and
234
+ * goes back within the same task, so it never shows.
235
+ */
236
+ const measure = (el) => {
237
+ const inline = el.style.inlineSize;
238
+ el.style.removeProperty("inline-size");
239
+ const width = el.getBoundingClientRect().width;
240
+ el.style.setProperty("inline-size", inline);
241
+ return width;
242
+ };
243
+ /** Sets every button's target width. `instant` skips the springs, for changes of size. */
244
+ const layout = (instant = false) => {
245
+ const all = items();
246
+ for (const el of all) natural.set(el, measure(el));
247
+ const targets = new Map(all.map((el) => [el, natural.get(el)]));
248
+ const pressed = all.findIndex((el) => el.hasAttribute("data-pressed"));
249
+ const standard = root.getAttribute("data-variant") !== "connected";
250
+ if (pressed !== -1 && standard && !prefersReducedMotion()) {
251
+ const growth = natural.get(all[pressed]) * ratio;
252
+ const neighbors = [all[pressed - 1], all[pressed + 1]].filter((el) => el !== void 0);
253
+ let gained = 0;
254
+ for (const el of neighbors) {
255
+ const width = natural.get(el);
256
+ const given = Math.min(growth / neighbors.length, width * ratio);
257
+ targets.set(el, width - given);
258
+ gained += given;
259
+ }
260
+ targets.set(all[pressed], natural.get(all[pressed]) + gained);
261
+ }
262
+ const params = readSpringParams(root, "fast-spatial");
263
+ for (const [el, target] of targets) {
264
+ const spring = springOf(el);
265
+ if (!el.style.inlineSize && spring.value !== natural.get(el)) spring.jump(natural.get(el));
266
+ if (spring.target === target) continue;
267
+ if (params && !instant && !prefersReducedMotion()) spring.to(target, params);
268
+ else spring.jump(target);
269
+ }
270
+ };
271
+ const observer = new MutationObserver((mutations) => {
272
+ const own = mutations.filter((m) => m.target.parentElement === root);
273
+ if (own.length > 0) layout(own.some((m) => m.attributeName !== "data-pressed"));
274
+ });
275
+ observer.observe(root, {
276
+ subtree: true,
277
+ attributes: true,
278
+ attributeFilter: [
279
+ "data-pressed",
280
+ "data-size",
281
+ "data-width"
282
+ ]
283
+ });
284
+ return { detach() {
285
+ root.removeEventListener("keydown", onKeydown);
286
+ observer.disconnect();
287
+ for (const [el, spring] of springs) {
288
+ spring.stop();
289
+ setWidth(el, void 0);
290
+ }
291
+ } };
292
+ }
1
293
  // src/button/button.ts
2
294
  const BUTTON_VARIANTS = [
3
295
  "filled",
@@ -24,28 +316,63 @@ const buttonDefaults = {
24
316
  softDisabled: false,
25
317
  type: "button"
26
318
  };
319
+ /** How the adapter renders the button's parts. The icon holds the leading icon. */
320
+ const buttonSkeleton = {
321
+ part: "root",
322
+ tag: "button",
323
+ children: [
324
+ {
325
+ part: "touch",
326
+ tag: "span"
327
+ },
328
+ {
329
+ part: "icon",
330
+ tag: "span",
331
+ when: "showsIcon",
332
+ content: true
333
+ },
334
+ {
335
+ part: "label",
336
+ tag: "span",
337
+ content: true
338
+ }
339
+ ]
340
+ };
27
341
  const isToggle = (p) => !!p.toggle && p.variant !== "text";
28
342
  /** Defaults under the given props, skipping props that are explicitly `undefined`. */
29
343
  const resolve = (props) => ({
30
344
  ...buttonDefaults,
31
- ...Object.fromEntries(Object.entries(props).filter(([, v]) => v !== void 0))
345
+ ...omitUndefined(props)
32
346
  });
33
347
  function createButton(options) {
34
348
  return {
35
- rootAttrs() {
36
- const p = resolve(options.props());
37
- const toggle = isToggle(p);
38
- return {
39
- type: p.type,
40
- disabled: p.disabled ? "" : void 0,
41
- "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
42
- "aria-pressed": toggle ? String(p.selected) : void 0,
43
- "data-variant": p.variant,
44
- "data-size": p.size,
45
- "data-shape": p.shape,
46
- "data-selected": toggle ? String(p.selected) : void 0
47
- };
349
+ parts: {
350
+ root() {
351
+ const p = resolve(options.props());
352
+ const toggle = isToggle(p);
353
+ return {
354
+ class: "rio-button rio-state-layer rio-focus-ring",
355
+ type: p.type,
356
+ disabled: p.disabled ? "" : void 0,
357
+ "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
358
+ "aria-pressed": toggle ? String(p.selected) : void 0,
359
+ "data-variant": p.variant,
360
+ "data-size": p.size,
361
+ "data-shape": p.shape,
362
+ "data-selected": toggle ? String(p.selected) : void 0
363
+ };
364
+ },
365
+ touch: () => ({
366
+ class: "rio-button__touch",
367
+ "aria-hidden": "true"
368
+ }),
369
+ icon: () => ({
370
+ class: "rio-button__icon",
371
+ "aria-hidden": "true"
372
+ }),
373
+ label: () => ({ class: "rio-button__label" })
48
374
  },
375
+ showsIcon: () => !!options.content?.().icon,
49
376
  handleClick(event) {
50
377
  const p = options.props();
51
378
  if (p.disabled || p.softDisabled) {
@@ -53,7 +380,7 @@ function createButton(options) {
53
380
  return;
54
381
  }
55
382
  options.onClick(event);
56
- if (isToggle(p)) options.onSelectedChange(!p.selected);
383
+ if (isToggle(p)) options.onChange(!p.selected);
57
384
  }
58
385
  };
59
386
  }
@@ -81,23 +408,50 @@ const iconButtonDefaults = {
81
408
  softDisabled: false,
82
409
  type: "button"
83
410
  };
411
+ /**
412
+ * How the adapter renders the icon button's parts. The icon holds the selected icon when
413
+ * `showsSelectedIcon()`, otherwise the default one.
414
+ */
415
+ const iconButtonSkeleton = {
416
+ part: "root",
417
+ tag: "button",
418
+ children: [{
419
+ part: "touch",
420
+ tag: "span"
421
+ }, {
422
+ part: "icon",
423
+ tag: "span",
424
+ content: true
425
+ }]
426
+ };
84
427
  function createIconButton(options) {
85
428
  const button = createButton({
86
- ...options,
87
429
  props: () => ({
88
430
  ...options.props(),
89
431
  variant: options.props().variant
90
- })
432
+ }),
433
+ onClick: options.onClick,
434
+ onChange: options.onChange
91
435
  });
92
436
  return {
93
- rootAttrs() {
437
+ parts: {
438
+ root() {
439
+ const p = options.props();
440
+ const root = button.parts.root();
441
+ return {
442
+ ...root,
443
+ class: `${root.class} rio-icon-button`,
444
+ "aria-label": p.label,
445
+ "data-width": p.width ?? iconButtonDefaults.width,
446
+ "data-variant": p.variant ?? iconButtonDefaults.variant
447
+ };
448
+ },
449
+ touch: button.parts.touch,
450
+ icon: button.parts.icon
451
+ },
452
+ showsSelectedIcon() {
94
453
  const p = options.props();
95
- return {
96
- ...button.rootAttrs(),
97
- "aria-label": p.label,
98
- "data-width": p.width ?? iconButtonDefaults.width,
99
- "data-variant": p.variant ?? iconButtonDefaults.variant
100
- };
454
+ return !!(p.toggle && p.selected && options.content?.().selectedIcon);
101
455
  },
102
456
  handleClick: button.handleClick
103
457
  };
@@ -219,7 +573,7 @@ const PADDING = 10;
219
573
  const SOFT_EDGE_RATIO = .35;
220
574
  const SOFT_EDGE_MINIMUM = 75;
221
575
  const isDisabled = (el) => el.matches(":disabled, [aria-disabled=\"true\"]");
222
- const reducedMotion$1 = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
576
+ const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
223
577
  const setPressed = (els, pressed) => {
224
578
  for (const el of els) if (pressed) el.dataset.pressed = "";
225
579
  else delete el.dataset.pressed;
@@ -264,7 +618,7 @@ function attachRipple(element, options = {}) {
264
618
  animation?.cancel();
265
619
  animation = surface.animate([frame(from, radius.from), frame(to, radius.to)], {
266
620
  pseudoElement: "::after",
267
- duration: reducedMotion$1() ? 0 : GROW_MS,
621
+ duration: reducedMotion() ? 0 : GROW_MS,
268
622
  easing: GROW_EASING,
269
623
  fill: "forwards"
270
624
  });
@@ -337,138 +691,13 @@ function attachRipple(element, options = {}) {
337
691
  element.removeEventListener("click", onClick);
338
692
  } };
339
693
  }
340
- // src/primitives/spring/spring.ts
341
- /**
342
- * Where a spring is `t` seconds after it starts at `from` with `velocity`, heading to `to`.
343
- */
344
- function solveSpring({ dampingRatio: zeta, stiffness }, from, to, velocity, t) {
345
- const w0 = Math.sqrt(stiffness);
346
- const x0 = from - to;
347
- const v0 = velocity;
348
- if (Math.abs(zeta - 1) < 1e-6) {
349
- const b = v0 + w0 * x0;
350
- const decay = Math.exp(-w0 * t);
351
- return {
352
- value: to + (x0 + b * t) * decay,
353
- velocity: (b - w0 * (x0 + b * t)) * decay
354
- };
355
- }
356
- if (zeta < 1) {
357
- const a = zeta * w0;
358
- const wd = w0 * Math.sqrt(1 - zeta * zeta);
359
- const b = (v0 + a * x0) / wd;
360
- const decay = Math.exp(-a * t);
361
- const cos = Math.cos(wd * t);
362
- const sin = Math.sin(wd * t);
363
- return {
364
- value: to + decay * (x0 * cos + b * sin),
365
- velocity: decay * ((wd * b - a * x0) * cos - (a * b + wd * x0) * sin)
366
- };
367
- }
368
- const root = w0 * Math.sqrt(zeta * zeta - 1);
369
- const r1 = -zeta * w0 + root;
370
- const r2 = -zeta * w0 - root;
371
- const c2 = (v0 - r1 * x0) / (r2 - r1);
372
- const c1 = x0 - c2;
373
- const e1 = Math.exp(r1 * t);
374
- const e2 = Math.exp(r2 * t);
375
- return {
376
- value: to + c1 * e1 + c2 * e2,
377
- velocity: r1 * c1 * e1 + r2 * c2 * e2
378
- };
379
- }
380
- const now = () => performance.now() / 1e3;
381
- /** A spring that animates a single number with `requestAnimationFrame`. */
382
- function createSpring(options) {
383
- const precision = options.precision ?? .01;
384
- let from = options.value;
385
- let target = options.value;
386
- let startVelocity = 0;
387
- let params;
388
- let startedAt = 0;
389
- let frame;
390
- const state = () => params && frame !== void 0 ? solveSpring(params, from, target, startVelocity, now() - startedAt) : {
391
- value: target,
392
- velocity: 0
393
- };
394
- const settle = () => {
395
- if (frame !== void 0) cancelAnimationFrame(frame);
396
- frame = void 0;
397
- from = target;
398
- startVelocity = 0;
399
- options.onUpdate(target);
400
- options.onRest?.();
401
- };
402
- const tick = () => {
403
- const { value, velocity } = state();
404
- if (Math.abs(value - target) < precision && Math.abs(velocity) < precision * 10) {
405
- settle();
406
- return;
407
- }
408
- options.onUpdate(value);
409
- frame = requestAnimationFrame(tick);
410
- };
411
- return {
412
- get value() {
413
- return state().value;
414
- },
415
- get velocity() {
416
- return state().velocity;
417
- },
418
- get target() {
419
- return target;
420
- },
421
- get moving() {
422
- return frame !== void 0;
423
- },
424
- to(next, nextParams, velocity) {
425
- const current = state();
426
- from = current.value;
427
- startVelocity = velocity ?? current.velocity;
428
- target = next;
429
- params = nextParams;
430
- startedAt = now();
431
- if (frame === void 0) frame = requestAnimationFrame(tick);
432
- },
433
- jump(value) {
434
- target = value;
435
- settle();
436
- },
437
- stop() {
438
- const { value } = state();
439
- if (frame !== void 0) cancelAnimationFrame(frame);
440
- frame = void 0;
441
- from = target = value;
442
- startVelocity = 0;
443
- }
444
- };
445
- }
446
- /**
447
- * Reads a motion token's spring from CSS variables on an element, for example
448
- * `--rio-motion-fast-spatial-damping` and `-stiffness`. Reading them from the element, rather than
449
- * from the tokens package, lets a subtree that swaps motion schemes change its springs too.
450
- * Returns `undefined` when the theme doesn't define them.
451
- */
452
- function readSpringParams(element, token) {
453
- const style = getComputedStyle(element);
454
- const dampingRatio = parseFloat(style.getPropertyValue(`--rio-motion-${token}-damping`));
455
- const stiffness = parseFloat(style.getPropertyValue(`--rio-motion-${token}-stiffness`));
456
- return Number.isFinite(dampingRatio) && Number.isFinite(stiffness) ? {
457
- dampingRatio,
458
- stiffness
459
- } : void 0;
460
- }
461
694
  // src/primitives/spring-property/spring-property.ts
462
- /** The data attributes Rio's components use for state. */
463
- const STATE_ATTRIBUTES = [
464
- "data-pressed",
465
- "data-dragging",
466
- "data-selected",
467
- "data-shape",
468
- "data-size",
469
- "data-icon"
695
+ /** The transition events that report a change of the target. */
696
+ const EVENTS = [
697
+ "transitionrun",
698
+ "transitionend",
699
+ "transitioncancel"
470
700
  ];
471
- const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
472
701
  function attachSpringProperty(element, options) {
473
702
  const token = options.token ?? "fast-spatial";
474
703
  const read = () => parseFloat(getComputedStyle(element).getPropertyValue(options.target));
@@ -485,16 +714,20 @@ function attachSpringProperty(element, options) {
485
714
  const target = read();
486
715
  if (!Number.isFinite(target)) return;
487
716
  const params = readSpringParams(element, token);
488
- if (!params || reducedMotion()) spring.jump(target);
717
+ if (!params || prefersReducedMotion()) spring.jump(target);
489
718
  else spring.to(target, params, velocity);
490
719
  };
491
- const observer = new MutationObserver(() => {
720
+ const onTransition = (event) => {
721
+ if (event.target !== element || event.propertyName !== options.target) return;
492
722
  if (!held && read() !== spring.target) go();
723
+ };
724
+ for (const type of EVENTS) element.addEventListener(type, onTransition);
725
+ const resync = new ResizeObserver(() => {
726
+ if (held || spring.moving) return;
727
+ const now = read();
728
+ if (Number.isFinite(now) && now !== spring.target) spring.jump(now);
493
729
  });
494
- observer.observe(element, {
495
- attributes: true,
496
- attributeFilter: options.watch ?? STATE_ATTRIBUTES
497
- });
730
+ resync.observe(element);
498
731
  return {
499
732
  get value() {
500
733
  return spring.value;
@@ -508,25 +741,182 @@ function attachSpringProperty(element, options) {
508
741
  go(velocity);
509
742
  },
510
743
  detach() {
511
- observer.disconnect();
744
+ for (const type of EVENTS) element.removeEventListener(type, onTransition);
745
+ resync.disconnect();
512
746
  spring.stop();
513
747
  options.apply(void 0);
514
748
  }
515
749
  };
516
750
  }
517
751
  // src/primitives/shape-morph/shape-morph.ts
752
+ const CORNERS = {
753
+ start: ["border-start-start-radius", "border-end-start-radius"],
754
+ end: ["border-start-end-radius", "border-end-end-radius"]
755
+ };
518
756
  function attachShapeMorph(element, options = {}) {
519
- const radius = attachSpringProperty(element, {
520
- ...options,
521
- target: "--rio-shape-morph-target",
522
- apply: (value) => value === void 0 ? element.style.removeProperty("border-radius") : element.style.setProperty("border-radius", `${value}px`)
523
- });
524
757
  element.dataset.shapeMorph = "";
758
+ const springs = ["start", "end"].map((side) => attachSpringProperty(element, {
759
+ ...options,
760
+ target: `--rio-shape-morph-${side}-target`,
761
+ apply: (value) => {
762
+ for (const corner of CORNERS[side]) if (value === void 0) element.style.removeProperty(corner);
763
+ else element.style.setProperty(corner, `${value}px`);
764
+ }
765
+ }));
525
766
  return { detach() {
526
- radius.detach();
767
+ for (const spring of springs) spring.detach();
527
768
  delete element.dataset.shapeMorph;
528
769
  } };
529
770
  }
771
+ // src/split-button/menu-icon.ts
772
+ const inlineVar$1 = (el, name, unit) => (value) => value === void 0 ? el.style.removeProperty(name) : el.style.setProperty(name, `${value}${unit}`);
773
+ /** Attach to the trailing button. */
774
+ function attachSplitButtonMenuIcon(trailing) {
775
+ const token = "standard-fast-spatial";
776
+ const turn = attachSpringProperty(trailing, {
777
+ target: "--rio-split-button-menu-turn-target",
778
+ token,
779
+ apply: inlineVar$1(trailing, "--_menu-turn", "deg")
780
+ });
781
+ const shift = attachSpringProperty(trailing, {
782
+ target: "--rio-split-button-menu-shift-target",
783
+ token,
784
+ apply: inlineVar$1(trailing, "--_menu-shift", "px")
785
+ });
786
+ return { detach() {
787
+ turn.detach();
788
+ shift.detach();
789
+ } };
790
+ }
791
+ // src/split-button/split-button.ts
792
+ /** Split button colors: the button's, without text. */
793
+ const SPLIT_BUTTON_VARIANTS = [
794
+ "filled",
795
+ "tonal",
796
+ "outlined",
797
+ "elevated"
798
+ ];
799
+ const splitButtonDefaults = {
800
+ variant: "filled",
801
+ size: "small",
802
+ open: false,
803
+ popup: "menu",
804
+ disabled: false,
805
+ softDisabled: false,
806
+ type: "button"
807
+ };
808
+ /**
809
+ * How the adapter renders the split button's parts. The icon and the label hold the leading
810
+ * button's content; the menu icon holds the expand icon the app provides.
811
+ */
812
+ const splitButtonSkeleton = {
813
+ part: "root",
814
+ tag: "div",
815
+ children: [{
816
+ part: "leading",
817
+ tag: "button",
818
+ children: [
819
+ {
820
+ part: "touch",
821
+ tag: "span"
822
+ },
823
+ {
824
+ part: "icon",
825
+ tag: "span",
826
+ when: "showsIcon",
827
+ content: true
828
+ },
829
+ {
830
+ part: "label",
831
+ tag: "span",
832
+ when: "showsLabel",
833
+ content: true
834
+ }
835
+ ]
836
+ }, {
837
+ part: "trailing",
838
+ tag: "button",
839
+ children: [{
840
+ part: "touch",
841
+ tag: "span"
842
+ }, {
843
+ part: "menuIcon",
844
+ tag: "span",
845
+ content: true
846
+ }]
847
+ }]
848
+ };
849
+ function createSplitButton(options) {
850
+ const props = () => ({
851
+ ...splitButtonDefaults,
852
+ ...omitUndefined(options.props())
853
+ });
854
+ const content = () => options.content?.() ?? {
855
+ icon: false,
856
+ label: true
857
+ };
858
+ const shared = () => {
859
+ const p = props();
860
+ return {
861
+ variant: p.variant,
862
+ size: p.size,
863
+ disabled: p.disabled,
864
+ softDisabled: p.softDisabled
865
+ };
866
+ };
867
+ const leading = createButton({
868
+ props: () => ({
869
+ ...shared(),
870
+ type: props().type
871
+ }),
872
+ onClick: options.onClick,
873
+ onChange: () => {}
874
+ });
875
+ const trailing = createButton({
876
+ props: () => ({
877
+ ...shared(),
878
+ type: "button"
879
+ }),
880
+ onClick: () => options.onChange(!props().open),
881
+ onChange: () => {}
882
+ });
883
+ return {
884
+ parts: {
885
+ root: () => ({ class: "rio-split-button" }),
886
+ leading() {
887
+ const root = leading.parts.root();
888
+ return {
889
+ ...root,
890
+ class: `${root.class} rio-split-button__leading`,
891
+ "aria-label": content().label ? void 0 : props().label
892
+ };
893
+ },
894
+ trailing() {
895
+ const p = props();
896
+ const root = trailing.parts.root();
897
+ return {
898
+ ...root,
899
+ class: `${root.class} rio-split-button__trailing`,
900
+ "aria-label": p.menuLabel,
901
+ "aria-haspopup": p.popup,
902
+ "aria-expanded": String(p.open),
903
+ "data-open": p.open ? "" : void 0
904
+ };
905
+ },
906
+ touch: leading.parts.touch,
907
+ icon: leading.parts.icon,
908
+ label: leading.parts.label,
909
+ menuIcon: () => ({
910
+ class: "rio-split-button__menu-icon",
911
+ "aria-hidden": "true"
912
+ })
913
+ },
914
+ showsIcon: () => content().icon,
915
+ showsLabel: () => content().label,
916
+ handleLeadingClick: leading.handleClick,
917
+ handleTrailingClick: trailing.handleClick
918
+ };
919
+ }
530
920
  // src/switch/handle.ts
531
921
  /** A release faster than this (px/s) picks the side it's heading to, wherever the handle is. */
532
922
  const FLING_VELOCITY = 300;
@@ -559,7 +949,7 @@ function attachSwitchHandle(root, options) {
559
949
  onEnd({ velocityX }) {
560
950
  const velocity = direction * velocityX;
561
951
  const next = Math.abs(velocity) > FLING_VELOCITY ? velocity > 0 : x.value > (off + on) / 2;
562
- if (next !== options.selected()) options.onSelectedChange(next);
952
+ if (next !== options.selected()) options.onChange(next);
563
953
  x.release(velocity);
564
954
  },
565
955
  onCancel() {
@@ -578,28 +968,80 @@ const switchDefaults = {
578
968
  disabled: false,
579
969
  softDisabled: false
580
970
  };
971
+ /**
972
+ * How the adapter renders the switch's parts. The state part is the circle where the state layer
973
+ * and the ripple draw. The icon holds the selected icon when `showsSelectedIcon()`, otherwise the
974
+ * unselected one.
975
+ */
976
+ const switchSkeleton = {
977
+ part: "root",
978
+ tag: "button",
979
+ children: [
980
+ {
981
+ part: "touch",
982
+ tag: "span"
983
+ },
984
+ {
985
+ part: "state",
986
+ tag: "span"
987
+ },
988
+ {
989
+ part: "handle",
990
+ tag: "span",
991
+ children: [{
992
+ part: "icon",
993
+ tag: "span",
994
+ when: "showsIcon",
995
+ content: true
996
+ }]
997
+ }
998
+ ]
999
+ };
581
1000
  function createSwitch(options) {
1001
+ const showsIcon = () => {
1002
+ const content = options.content?.();
1003
+ return !!(options.props().selected ? content?.selectedIcon : content?.unselectedIcon);
1004
+ };
582
1005
  return {
583
- rootAttrs() {
584
- const p = options.props();
585
- const selected = String(!!p.selected);
586
- return {
587
- type: "button",
588
- role: "switch",
589
- "aria-checked": selected,
590
- disabled: p.disabled ? "" : void 0,
591
- "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
592
- "data-selected": selected
593
- };
1006
+ parts: {
1007
+ root() {
1008
+ const p = options.props();
1009
+ const selected = String(!!p.selected);
1010
+ return {
1011
+ class: "rio-switch rio-focus-ring",
1012
+ type: "button",
1013
+ role: "switch",
1014
+ "aria-checked": selected,
1015
+ disabled: p.disabled ? "" : void 0,
1016
+ "aria-disabled": p.softDisabled && !p.disabled ? "true" : void 0,
1017
+ "data-selected": selected,
1018
+ "data-icon": showsIcon() ? "" : void 0
1019
+ };
1020
+ },
1021
+ touch: () => ({
1022
+ class: "rio-switch__touch",
1023
+ "aria-hidden": "true"
1024
+ }),
1025
+ state: () => ({
1026
+ class: "rio-switch__state",
1027
+ "aria-hidden": "true"
1028
+ }),
1029
+ handle: () => ({
1030
+ class: "rio-switch__handle",
1031
+ "aria-hidden": "true"
1032
+ }),
1033
+ icon: () => ({ class: "rio-switch__icon" })
594
1034
  },
1035
+ showsIcon,
1036
+ showsSelectedIcon: () => !!(options.props().selected && options.content?.().selectedIcon),
595
1037
  handleClick(event) {
596
1038
  const p = options.props();
597
1039
  if (p.disabled || p.softDisabled) {
598
1040
  event.preventDefault();
599
1041
  return;
600
1042
  }
601
- options.onSelectedChange(!p.selected);
1043
+ options.onChange(!p.selected);
602
1044
  }
603
1045
  };
604
1046
  }
605
- export { BUTTON_SIZES, BUTTON_VARIANTS, ICON_BUTTON_VARIANTS, ICON_BUTTON_WIDTHS, STATE_ATTRIBUTES, attachDrag, attachRipple, attachShapeMorph, attachSpringProperty, attachSwitchHandle, buttonDefaults, createButton, createIconButton, createSpring, createSwitch, iconButtonDefaults, readSpringParams, solveSpring, switchDefaults };
1047
+ export { BUTTON_GROUP_SELECTIONS, BUTTON_GROUP_VARIANTS, BUTTON_SIZES, BUTTON_VARIANTS, ICON_BUTTON_VARIANTS, ICON_BUTTON_WIDTHS, SPLIT_BUTTON_VARIANTS, attachButtonGroup, attachDrag, attachRipple, attachShapeMorph, attachSplitButtonMenuIcon, attachSpringProperty, attachSwitchHandle, buttonDefaults, buttonGroupDefaults, buttonGroupSkeleton, buttonSkeleton, createButton, createButtonGroup, createIconButton, createSplitButton, createSpring, createSwitch, iconButtonDefaults, iconButtonSkeleton, prefersReducedMotion, readSpringParams, solveSpring, splitButtonDefaults, splitButtonSkeleton, switchDefaults, switchSkeleton };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rio-m3e/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-agnostic state, props and behaviors for Rio",
5
5
  "license": "Apache-2.0",
6
6
  "author": "LegenLee",