gbs-add-block 1.2.18 → 1.2.19

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 (41) hide show
  1. package/README.md +2 -2
  2. package/index.cjs +5 -0
  3. package/package.json +1 -1
  4. package/source/beta-components/number-input/README.md +53 -0
  5. package/source/beta-components/number-input/__tests__/core.test.ts +151 -0
  6. package/source/beta-components/number-input/core/format.ts +37 -0
  7. package/source/beta-components/number-input/core/index.ts +4 -0
  8. package/source/beta-components/number-input/core/parse.ts +118 -0
  9. package/source/beta-components/number-input/core/step.ts +73 -0
  10. package/source/beta-components/number-input/core/types.ts +32 -0
  11. package/source/beta-components/number-input/index.ts +5 -0
  12. package/source/beta-components/number-input/react/NumberInput.tsx +465 -0
  13. package/source/beta-components/number-input/react/icons.tsx +17 -0
  14. package/source/beta-components/number-input/react/locale.ts +7 -0
  15. package/source/beta-components/number-input/react/props.ts +11 -0
  16. package/source/beta-components/number-input/styles.css +231 -0
  17. package/source/beta-components/radio-group/README.md +58 -0
  18. package/source/beta-components/radio-group/__tests__/core.test.ts +64 -0
  19. package/source/beta-components/radio-group/core/group.ts +36 -0
  20. package/source/beta-components/radio-group/core/index.ts +2 -0
  21. package/source/beta-components/radio-group/core/types.ts +20 -0
  22. package/source/beta-components/radio-group/index.ts +9 -0
  23. package/source/beta-components/radio-group/react/Radio.tsx +144 -0
  24. package/source/beta-components/radio-group/react/RadioGroup.tsx +179 -0
  25. package/source/beta-components/radio-group/react/context.ts +18 -0
  26. package/source/beta-components/radio-group/react/locale.ts +5 -0
  27. package/source/beta-components/radio-group/react/props.ts +12 -0
  28. package/source/beta-components/radio-group/styles.css +270 -0
  29. package/source/beta-components/shared/__tests__/position.test.ts +143 -170
  30. package/source/beta-components/shared/core/position.ts +2 -15
  31. package/source/beta-components/shared/react/Popover.tsx +5 -18
  32. package/source/beta-components/switch/README.md +46 -0
  33. package/source/beta-components/switch/__tests__/core.test.ts +64 -0
  34. package/source/beta-components/switch/core/index.ts +2 -0
  35. package/source/beta-components/switch/core/toggle.ts +54 -0
  36. package/source/beta-components/switch/core/types.ts +9 -0
  37. package/source/beta-components/switch/index.ts +5 -0
  38. package/source/beta-components/switch/react/Switch.tsx +219 -0
  39. package/source/beta-components/switch/react/locale.ts +5 -0
  40. package/source/beta-components/switch/react/props.ts +12 -0
  41. package/source/beta-components/switch/styles.css +229 -0
@@ -1,170 +1,143 @@
1
- import { describe, expect, it } from "vitest";
2
- import { placePopover, type AnchorRect, type PlacementOptions } from "../core/position";
3
-
4
- const VIEWPORT = { width: 1000, height: 800 };
5
-
6
- const anchorAt = (left: number, top: number, width = 120, height = 32): AnchorRect => ({
7
- left,
8
- top,
9
- width,
10
- height,
11
- right: left + width,
12
- bottom: top + height,
13
- });
14
-
15
- const options = (overrides: Partial<PlacementOptions> = {}): PlacementOptions => ({
16
- side: "bottom",
17
- align: "start",
18
- gap: 4,
19
- edge: 8,
20
- rtl: false,
21
- fitHeight: false,
22
- minHeight: 140,
23
- ...overrides,
24
- });
25
-
26
- describe("below the anchor", () => {
27
- it("sits under the anchor, aligned to its left edge", () => {
28
- const placement = placePopover(anchorAt(100, 100), { width: 200, height: 150 }, VIEWPORT, options());
29
- expect(placement).toMatchObject({ side: "bottom", left: 100, top: 136 });
30
- });
31
-
32
- it("aligns to the right edge when asked", () => {
33
- const placement = placePopover(
34
- anchorAt(100, 100),
35
- { width: 200, height: 150 },
36
- VIEWPORT,
37
- options({ align: "end" }),
38
- );
39
- // The anchor's right edge is 220, so a 200-wide panel starts at 20.
40
- expect(placement.left).toBe(20);
41
- });
42
-
43
- it("moves above when there is no room below and it fits above", () => {
44
- const placement = placePopover(anchorAt(100, 700), { width: 200, height: 150 }, VIEWPORT, options());
45
- expect(placement).toMatchObject({ side: "top", top: 546 });
46
- });
47
-
48
- it("stays below when it fits neither side, rather than running off the top", () => {
49
- const placement = placePopover(anchorAt(100, 300), { width: 200, height: 780 }, VIEWPORT, options());
50
- expect(placement.side).toBe("bottom");
51
- });
52
-
53
- it("pulls a panel that fits neither side back inside the viewport", () => {
54
- // 780 tall against 456px below and 288px above: it cannot sit under the
55
- // anchor without its foot leaving the screen, and nothing in the top layer
56
- // can be scrolled into view.
57
- const placement = placePopover(anchorAt(100, 300), { width: 200, height: 780 }, VIEWPORT, options());
58
- expect(placement.top).toBe(800 - 780 - 8);
59
- expect(placement.top + 780).toBeLessThanOrEqual(800);
60
- });
61
-
62
- it("keeps the top gutter when the panel is taller than the viewport", () => {
63
- const placement = placePopover(anchorAt(100, 300), { width: 200, height: 900 }, VIEWPORT, options());
64
- expect(placement.top).toBe(8);
65
- });
66
- });
67
-
68
- describe("fitHeight", () => {
69
- it("takes the roomier side and caps the height to it", () => {
70
- // 700px from the top: 68px below, 688px above.
71
- const placement = placePopover(
72
- anchorAt(100, 700),
73
- { width: 200, height: 600 },
74
- VIEWPORT,
75
- options({ fitHeight: true }),
76
- );
77
- expect(placement.side).toBe("top");
78
- expect(placement.maxHeight).toBe(688);
79
- });
80
-
81
- it("never caps below minHeight, however cramped the viewport", () => {
82
- const placement = placePopover(
83
- anchorAt(100, 380),
84
- { width: 200, height: 300 },
85
- { width: 1000, height: 420 },
86
- options({ fitHeight: true, minHeight: 140 }),
87
- );
88
- expect(placement.maxHeight).toBeGreaterThanOrEqual(140);
89
- });
90
- });
91
-
92
- describe("staying on screen", () => {
93
- it("pulls a panel back from the right edge", () => {
94
- const placement = placePopover(anchorAt(950, 100), { width: 300, height: 100 }, VIEWPORT, options());
95
- expect(placement.left).toBe(1000 - 300 - 8);
96
- });
97
-
98
- it("pulls a side panel back inside when it fits on neither side", () => {
99
- // 600 wide with 448px to the right and 488px to the left: too wide to flip,
100
- // so without a clamp its right edge would sit 144px past the viewport.
101
- const placement = placePopover(
102
- anchorAt(500, 300, 40),
103
- { width: 600, height: 100 },
104
- VIEWPORT,
105
- options({ side: "right" }),
106
- );
107
- expect(placement.side).toBe("right");
108
- expect(placement.left).toBe(1000 - 600 - 8);
109
- });
110
-
111
- it("keeps the left gutter when the panel is wider than the viewport", () => {
112
- const placement = placePopover(anchorAt(10, 100), { width: 1200, height: 100 }, VIEWPORT, options());
113
- expect(placement.left).toBe(8);
114
- });
115
- });
116
-
117
- describe("sideways", () => {
118
- it("opens to the right of the anchor, top edges level", () => {
119
- const placement = placePopover(
120
- anchorAt(100, 100),
121
- { width: 200, height: 150 },
122
- VIEWPORT,
123
- options({ side: "right" }),
124
- );
125
- expect(placement).toMatchObject({ side: "right", left: 224, top: 100 });
126
- });
127
-
128
- it("flips to the left when the right has no room", () => {
129
- const placement = placePopover(
130
- anchorAt(850, 100),
131
- { width: 200, height: 150 },
132
- VIEWPORT,
133
- options({ side: "right" }),
134
- );
135
- expect(placement).toMatchObject({ side: "left", left: 646 });
136
- });
137
-
138
- it("keeps a tall panel inside the viewport", () => {
139
- const placement = placePopover(
140
- anchorAt(100, 760),
141
- { width: 200, height: 400 },
142
- VIEWPORT,
143
- options({ side: "right" }),
144
- );
145
- expect(placement.top).toBe(800 - 400 - 8);
146
- });
147
- });
148
-
149
- describe("right to left", () => {
150
- it("mirrors which edge `start` means", () => {
151
- const placement = placePopover(
152
- anchorAt(100, 100),
153
- { width: 200, height: 150 },
154
- VIEWPORT,
155
- options({ rtl: true }),
156
- );
157
- // Reading starts on the right, so the panel's right edge meets the anchor's.
158
- expect(placement.left).toBe(20);
159
- });
160
-
161
- it("mirrors left and right", () => {
162
- const placement = placePopover(
163
- anchorAt(500, 100),
164
- { width: 200, height: 150 },
165
- VIEWPORT,
166
- options({ side: "right", rtl: true }),
167
- );
168
- expect(placement).toMatchObject({ side: "left", left: 296 });
169
- });
170
- });
1
+ import { describe, expect, it } from "vitest";
2
+ import { placePopover, type AnchorRect, type PlacementOptions } from "../core/position";
3
+
4
+ const VIEWPORT = { width: 1000, height: 800 };
5
+
6
+ const anchorAt = (left: number, top: number, width = 120, height = 32): AnchorRect => ({
7
+ left,
8
+ top,
9
+ width,
10
+ height,
11
+ right: left + width,
12
+ bottom: top + height,
13
+ });
14
+
15
+ const options = (overrides: Partial<PlacementOptions> = {}): PlacementOptions => ({
16
+ side: "bottom",
17
+ align: "start",
18
+ gap: 4,
19
+ edge: 8,
20
+ rtl: false,
21
+ fitHeight: false,
22
+ minHeight: 140,
23
+ ...overrides,
24
+ });
25
+
26
+ describe("below the anchor", () => {
27
+ it("sits under the anchor, aligned to its left edge", () => {
28
+ const placement = placePopover(anchorAt(100, 100), { width: 200, height: 150 }, VIEWPORT, options());
29
+ expect(placement).toMatchObject({ side: "bottom", left: 100, top: 136 });
30
+ });
31
+
32
+ it("aligns to the right edge when asked", () => {
33
+ const placement = placePopover(
34
+ anchorAt(100, 100),
35
+ { width: 200, height: 150 },
36
+ VIEWPORT,
37
+ options({ align: "end" }),
38
+ );
39
+ // The anchor's right edge is 220, so a 200-wide panel starts at 20.
40
+ expect(placement.left).toBe(20);
41
+ });
42
+
43
+ it("moves above when there is no room below and it fits above", () => {
44
+ const placement = placePopover(anchorAt(100, 700), { width: 200, height: 150 }, VIEWPORT, options());
45
+ expect(placement).toMatchObject({ side: "top", top: 546 });
46
+ });
47
+
48
+ it("stays below when it fits neither side, rather than running off the top", () => {
49
+ const placement = placePopover(anchorAt(100, 300), { width: 200, height: 780 }, VIEWPORT, options());
50
+ expect(placement.side).toBe("bottom");
51
+ });
52
+ });
53
+
54
+ describe("fitHeight", () => {
55
+ it("takes the roomier side and caps the height to it", () => {
56
+ // 700px from the top: 68px below, 688px above.
57
+ const placement = placePopover(
58
+ anchorAt(100, 700),
59
+ { width: 200, height: 600 },
60
+ VIEWPORT,
61
+ options({ fitHeight: true }),
62
+ );
63
+ expect(placement.side).toBe("top");
64
+ expect(placement.maxHeight).toBe(688);
65
+ });
66
+
67
+ it("never caps below minHeight, however cramped the viewport", () => {
68
+ const placement = placePopover(
69
+ anchorAt(100, 380),
70
+ { width: 200, height: 300 },
71
+ { width: 1000, height: 420 },
72
+ options({ fitHeight: true, minHeight: 140 }),
73
+ );
74
+ expect(placement.maxHeight).toBeGreaterThanOrEqual(140);
75
+ });
76
+ });
77
+
78
+ describe("staying on screen", () => {
79
+ it("pulls a panel back from the right edge", () => {
80
+ const placement = placePopover(anchorAt(950, 100), { width: 300, height: 100 }, VIEWPORT, options());
81
+ expect(placement.left).toBe(1000 - 300 - 8);
82
+ });
83
+
84
+ it("keeps the left gutter when the panel is wider than the viewport", () => {
85
+ const placement = placePopover(anchorAt(10, 100), { width: 1200, height: 100 }, VIEWPORT, options());
86
+ expect(placement.left).toBe(8);
87
+ });
88
+ });
89
+
90
+ describe("sideways", () => {
91
+ it("opens to the right of the anchor, top edges level", () => {
92
+ const placement = placePopover(
93
+ anchorAt(100, 100),
94
+ { width: 200, height: 150 },
95
+ VIEWPORT,
96
+ options({ side: "right" }),
97
+ );
98
+ expect(placement).toMatchObject({ side: "right", left: 224, top: 100 });
99
+ });
100
+
101
+ it("flips to the left when the right has no room", () => {
102
+ const placement = placePopover(
103
+ anchorAt(850, 100),
104
+ { width: 200, height: 150 },
105
+ VIEWPORT,
106
+ options({ side: "right" }),
107
+ );
108
+ expect(placement).toMatchObject({ side: "left", left: 646 });
109
+ });
110
+
111
+ it("keeps a tall panel inside the viewport", () => {
112
+ const placement = placePopover(
113
+ anchorAt(100, 760),
114
+ { width: 200, height: 400 },
115
+ VIEWPORT,
116
+ options({ side: "right" }),
117
+ );
118
+ expect(placement.top).toBe(800 - 400 - 8);
119
+ });
120
+ });
121
+
122
+ describe("right to left", () => {
123
+ it("mirrors which edge `start` means", () => {
124
+ const placement = placePopover(
125
+ anchorAt(100, 100),
126
+ { width: 200, height: 150 },
127
+ VIEWPORT,
128
+ options({ rtl: true }),
129
+ );
130
+ // Reading starts on the right, so the panel's right edge meets the anchor's.
131
+ expect(placement.left).toBe(20);
132
+ });
133
+
134
+ it("mirrors left and right", () => {
135
+ const placement = placePopover(
136
+ anchorAt(500, 100),
137
+ { width: 200, height: 150 },
138
+ VIEWPORT,
139
+ options({ side: "right", rtl: true }),
140
+ );
141
+ expect(placement).toMatchObject({ side: "left", left: 296 });
142
+ });
143
+ });
@@ -94,16 +94,7 @@ export function placePopover(
94
94
  edge,
95
95
  viewport.width - size.width - edge,
96
96
  ),
97
- // Clamped on both ends: when a panel fits neither above nor below, it is
98
- // pulled back inside instead of hanging off the edge. Nothing in the top
99
- // layer scrolls into view, so whatever leaves the viewport is lost. A
100
- // panel taller than the viewport itself still starts at the top gutter,
101
- // since `clamp` keeps the minimum when the maximum falls below it.
102
- top: clamp(
103
- below ? anchor.bottom + gap : anchor.top - gap - size.height,
104
- edge,
105
- viewport.height - size.height - edge,
106
- ),
97
+ top: Math.max(edge, below ? anchor.bottom + gap : anchor.top - gap - size.height),
107
98
  maxHeight: fitHeight
108
99
  ? Math.max(minHeight, below ? roomBelow : roomAbove)
109
100
  : undefined,
@@ -122,11 +113,7 @@ export function placePopover(
122
113
 
123
114
  return {
124
115
  side: right ? "right" : "left",
125
- left: clamp(
126
- right ? anchor.right + gap : anchor.left - gap - size.width,
127
- edge,
128
- viewport.width - size.width - edge,
129
- ),
116
+ left: Math.max(edge, right ? anchor.right + gap : anchor.left - gap - size.width),
130
117
  top: clamp(
131
118
  across(anchor.top, anchor.height, size.height),
132
119
  edge,
@@ -137,30 +137,17 @@ export function AnchoredPopover({
137
137
  target?.focus({ preventScroll: true });
138
138
  }
139
139
 
140
- // Scrolling fires far faster than the screen refreshes, and each pass
141
- // measures the anchor and writes styles back, so repositioning is coalesced
142
- // into one frame rather than thrashing layout on every event.
143
- let frame = 0;
144
- const schedule = () => {
145
- if (frame) return;
146
- frame = requestAnimationFrame(() => {
147
- frame = 0;
148
- position();
149
- });
150
- };
151
-
152
- const observer = observeResize ? new ResizeObserver(schedule) : null;
140
+ const observer = observeResize ? new ResizeObserver(position) : null;
153
141
  observer?.observe(el);
154
142
  if (observeResize === "both") observer?.observe(anchor);
155
- window.addEventListener("resize", schedule, { passive: true });
143
+ window.addEventListener("resize", position);
156
144
  // Capture: a scroll in any ancestor moves the anchor, not just the page.
157
- window.addEventListener("scroll", schedule, { capture: true, passive: true });
145
+ window.addEventListener("scroll", position, true);
158
146
 
159
147
  return () => {
160
- if (frame) cancelAnimationFrame(frame);
161
148
  observer?.disconnect();
162
- window.removeEventListener("resize", schedule);
163
- window.removeEventListener("scroll", schedule, true);
149
+ window.removeEventListener("resize", position);
150
+ window.removeEventListener("scroll", position, true);
164
151
  const hadFocus = el.contains(document.activeElement);
165
152
  if (el.matches(":popover-open")) el.hidePopover();
166
153
  if (returnFocus && (hadFocus || document.activeElement === document.body) && anchor.isConnected) {
@@ -0,0 +1,46 @@
1
+ # Switch
2
+
3
+ A setting that takes effect the moment it is flipped, for React 19, styled to
4
+ match the rest of the library. The control is a real
5
+ `<input type="checkbox" role="switch">`, so forms, form libraries and assistive
6
+ technology work as they do natively — and it is announced as "on"/"off" rather
7
+ than "checked". No runtime dependencies besides React.
8
+
9
+ ```ts
10
+ import { Switch } from "@/components/switch";
11
+ import "@/components/switch/styles.css";
12
+ ```
13
+
14
+ ```tsx
15
+ <Switch label="Email notifications" name="notify" checked={on} onCheckedChange={setOn} />
16
+
17
+ {/* Return a promise and the switch shows the new setting, spins while it saves,
18
+ and puts itself back if the save fails. */}
19
+ <Switch
20
+ label="Two-factor authentication"
21
+ description="Ask for a code from your authenticator app."
22
+ checked={enabled}
23
+ onCheckedChange={(next) => api.setTwoFactor(next)}
24
+ />
25
+
26
+ {/* A settings row: label first, switch against the right edge. */}
27
+ <Switch label="Weekly digest" labelPosition="start" defaultChecked />
28
+ ```
29
+
30
+ **Props:** `checked` / `defaultChecked` / `onCheckedChange` (may return a
31
+ promise), `value`, `label`, `description`, `error`, `size` (`sm` `md` `lg`),
32
+ `labelPosition` (`end` `start`), `loading`, `disabled`, `readOnly`, `required`,
33
+ `name`, `classNames` (`root` `control` `input` `track` `thumb` `label`
34
+ `description` `error`), `localeText`, `ref` (the `<input>`), and every
35
+ `<input>` attribute.
36
+
37
+ **Keyboard:** **Space** toggles. **→** turns it on and **←** off (mirrored in
38
+ right-to-left layouts), which is the ARIA switch pattern.
39
+
40
+ The `idleToggle`, `request`, `settle` and `adopt` state machine behind the
41
+ saving behaviour is exported from the framework-free `core`: it decides what a
42
+ failed save goes back to, and ignores the answer to a request that a later one
43
+ has already replaced.
44
+
45
+ Use a **Checkbox** instead when the value is part of a form that is submitted
46
+ later, and a Switch when flipping it *is* the action.
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { adopt, idleToggle, request, settle } from "../core/toggle";
3
+
4
+ describe("switch toggling", () => {
5
+ it("shows the new setting before it has been saved", () => {
6
+ const state = request(idleToggle(false), true);
7
+ expect(state).toEqual({ checked: true, pending: true, fallback: false });
8
+ });
9
+
10
+ it("keeps the new setting once the save succeeds", () => {
11
+ const state = settle(request(idleToggle(false), true), true, true);
12
+ expect(state).toEqual({ checked: true, pending: null, fallback: true });
13
+ });
14
+
15
+ it("goes back where it was when the save fails", () => {
16
+ const state = settle(request(idleToggle(false), true), true, false);
17
+ expect(state).toEqual({ checked: false, pending: null, fallback: false });
18
+ });
19
+
20
+ it("returns to where the run of changes started, not to the opposite", () => {
21
+ // On, then off again while the first save is still running: failing the
22
+ // second must land on the original off, which is also where it looks like
23
+ // it is — flipping the visible value would turn it back on.
24
+ const first = request(idleToggle(false), true);
25
+ const second = request(first, false);
26
+ expect(second.fallback).toBe(false);
27
+ expect(settle(second, false, false).checked).toBe(false);
28
+ });
29
+
30
+ it("ignores the outcome of a change that was overtaken", () => {
31
+ const first = request(idleToggle(false), true);
32
+ const second = request(first, false);
33
+ // The first request finally answers, long after the second replaced it.
34
+ expect(settle(second, true, true)).toBe(second);
35
+ expect(settle(second, true, false)).toBe(second);
36
+ });
37
+
38
+ it("ignores an outcome when nothing is in flight", () => {
39
+ const idle = idleToggle(true);
40
+ expect(settle(idle, true, false)).toBe(idle);
41
+ });
42
+
43
+ it("does nothing when asked for the value it already shows", () => {
44
+ const idle = idleToggle(true);
45
+ expect(request(idle, true)).toBe(idle);
46
+ });
47
+
48
+ it("re-requests a value that is showing but not yet saved", () => {
49
+ // Pressed on, pressed off, pressed on again: the last press still needs an
50
+ // outcome of its own, so it cannot be dropped as a no-op.
51
+ const pending = request(request(idleToggle(false), true), false);
52
+ const again = request(pending, true);
53
+ expect(again.pending).toBe(true);
54
+ expect(again.fallback).toBe(false);
55
+ });
56
+
57
+ it("lets a controlled value win over anything in flight", () => {
58
+ const pending = request(idleToggle(false), true);
59
+ expect(adopt(pending, false)).toEqual({ checked: false, pending: null, fallback: false });
60
+ // An unchanged, settled value is left as it is, so React skips the render.
61
+ const idle = idleToggle(true);
62
+ expect(adopt(idle, true)).toBe(idle);
63
+ });
64
+ });
@@ -0,0 +1,2 @@
1
+ export { adopt, idleToggle, request, settle, type ToggleState } from "./toggle";
2
+ export type * from "./types";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The state of a switch whose change has to be saved somewhere.
3
+ *
4
+ * A switch is usually the whole interaction: there is no form to submit
5
+ * afterwards, so flipping it *is* the request. That leaves three things to
6
+ * agree on, and they are worth keeping out of the component and under test.
7
+ *
8
+ * The switch moves as soon as it is pressed, rather than waiting for the
9
+ * server, because a control that ignores a press feels broken. If the save
10
+ * fails it goes back to where it was — to `fallback`, the value from before the
11
+ * press, not simply the opposite of what is on screen, since the two differ
12
+ * once anything else has changed the value in the meantime.
13
+ *
14
+ * And a request that is overtaken is ignored: `settle` only accepts an outcome
15
+ * for the change still in flight (`pending`), so a late failure from an
16
+ * abandoned request cannot drag a newer value back with it.
17
+ */
18
+
19
+ export interface ToggleState {
20
+ /** What the switch shows now. */
21
+ checked: boolean;
22
+ /** The change being saved, or null when nothing is in flight. */
23
+ pending: boolean | null;
24
+ /** What to go back to if the save fails. */
25
+ fallback: boolean;
26
+ }
27
+
28
+ export const idleToggle = (checked: boolean): ToggleState => ({
29
+ checked,
30
+ pending: null,
31
+ fallback: checked,
32
+ });
33
+
34
+ /** Shows `next` at once and remembers where to return to. */
35
+ export function request(state: ToggleState, next: boolean): ToggleState {
36
+ if (next === state.checked && state.pending === null) return state;
37
+ return { checked: next, pending: next, fallback: state.pending === null ? state.checked : state.fallback };
38
+ }
39
+
40
+ /**
41
+ * Applies the outcome of the change that is in flight. An outcome for anything
42
+ * else — a request that was already overtaken — leaves the state alone.
43
+ */
44
+ export function settle(state: ToggleState, requested: boolean, saved: boolean): ToggleState {
45
+ if (state.pending === null || state.pending !== requested) return state;
46
+ const checked = saved ? state.checked : state.fallback;
47
+ return { checked, pending: null, fallback: checked };
48
+ }
49
+
50
+ /** A value arriving from a controlled prop wins, and ends anything in flight. */
51
+ export function adopt(state: ToggleState, checked: boolean): ToggleState {
52
+ if (checked === state.checked && state.pending === null) return state;
53
+ return idleToggle(checked);
54
+ }
@@ -0,0 +1,9 @@
1
+ export type SwitchSize = "sm" | "md" | "lg";
2
+
3
+ /** Which side of the track the label sits on. */
4
+ export type SwitchLabelPosition = "end" | "start";
5
+
6
+ export interface SwitchLocaleText {
7
+ /** Announced while a change is being saved. */
8
+ saving: string;
9
+ }
@@ -0,0 +1,5 @@
1
+ export { Switch } from "./react/Switch";
2
+ export type { SwitchProps } from "./react/Switch";
3
+ export { defaultSwitchText } from "./react/locale";
4
+ export type { SwitchSlot } from "./react/props";
5
+ export * from "./core";