@passui/react 0.1.45 → 0.1.47

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 (141) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +38 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -402
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +278 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +374 -0
  72. package/src/components/popover/popover-root.tsx +97 -0
  73. package/src/components/popover/popover-trigger.tsx +71 -0
  74. package/src/components/profile-image/profile-image.tsx +80 -64
  75. package/src/components/progress-view/progress-view.tsx +61 -59
  76. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  77. package/src/components/radio-group/radio-group.tsx +71 -68
  78. package/src/components/search-field/expanding-search-field.tsx +191 -177
  79. package/src/components/search-field/search-field.tsx +59 -50
  80. package/src/components/search-list/search-list.tsx +246 -236
  81. package/src/components/section/section.tsx +58 -58
  82. package/src/components/secure-field/secure-field.tsx +3 -3
  83. package/src/components/segmented-control/segmented-control.tsx +136 -136
  84. package/src/components/sheet/modal-environment.ts +71 -64
  85. package/src/components/sheet/sheet-portal.tsx +58 -53
  86. package/src/components/sheet/sheet.tsx +146 -141
  87. package/src/components/sheet/use-sheet-focus.ts +97 -82
  88. package/src/components/sidebar/sidebar.tsx +78 -78
  89. package/src/components/skeleton/skeleton.tsx +30 -21
  90. package/src/components/slider/slider.tsx +113 -104
  91. package/src/components/slosh-slider/liquid-step.ts +32 -31
  92. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  93. package/src/components/stat-tile/index.ts +1 -1
  94. package/src/components/stat-tile/stat-tile.tsx +79 -68
  95. package/src/components/status-indicator/status-indicator.tsx +40 -37
  96. package/src/components/stepper/rolling-digit.tsx +82 -68
  97. package/src/components/stepper/stepper.tsx +122 -113
  98. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  99. package/src/components/tab-bar/tab-bar.tsx +149 -148
  100. package/src/components/tab-view/tab-view.tsx +140 -137
  101. package/src/components/table/table.tsx +117 -98
  102. package/src/components/text-editor/text-editor.tsx +34 -29
  103. package/src/components/text-field/text-field.tsx +182 -180
  104. package/src/components/toast/toast-store.ts +127 -115
  105. package/src/components/toast/toaster.tsx +361 -347
  106. package/src/components/toggle/toggle.tsx +297 -282
  107. package/src/components/tooltip/tooltip-host.tsx +157 -150
  108. package/src/components/view-controller/index.ts +1 -1
  109. package/src/components/view-controller/view-controller.tsx +64 -53
  110. package/src/components/wake-slider/wake-slider.tsx +220 -205
  111. package/src/hooks/scroll/minimize-decision.ts +11 -7
  112. package/src/hooks/use-anchored-disclosure.ts +19 -19
  113. package/src/hooks/use-anchored-placement.ts +29 -25
  114. package/src/hooks/use-dismiss.ts +33 -29
  115. package/src/hooks/use-floating-dismiss.ts +24 -22
  116. package/src/hooks/use-media-query.ts +67 -60
  117. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  118. package/src/hooks/use-visual-viewport.ts +63 -63
  119. package/src/hooks/viewport/types.ts +16 -16
  120. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  121. package/src/icons/icons.tsx +440 -328
  122. package/src/index.ts +91 -91
  123. package/src/lib/anchored-placement.ts +70 -65
  124. package/src/lib/class-names.ts +14 -13
  125. package/src/lib/clipboard.ts +8 -7
  126. package/src/lib/date.ts +61 -60
  127. package/src/lib/floating-layers.ts +39 -24
  128. package/src/lib/haptics.ts +10 -10
  129. package/src/lib/language.ts +9 -9
  130. package/src/lib/range-value.ts +27 -21
  131. package/src/lib/screen-edges.ts +33 -30
  132. package/src/lib/shapes.ts +38 -34
  133. package/src/machines/popover.machine.ts +51 -0
  134. package/src/motion/transitions.ts +46 -43
  135. package/src/styles/code-block.css +69 -43
  136. package/src/styles/motion-controls.css +393 -191
  137. package/src/tokens.ts +1 -1
  138. package/dist/types/src/components/popover/popover-fit.d.ts +0 -21
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover-fit.ts +0 -69
  141. package/src/components/popover/popover.tsx +0 -334
@@ -1,8 +1,8 @@
1
- import { clampRange as clamp } from "../../lib/range-value"
1
+ import { clampRange as clamp } from "../../lib/range-value";
2
2
  // Adapted from Bencho. See THIRD_PARTY_NOTICES.md for the MIT notice.
3
- import { type CSSProperties, useEffect, useRef, useState } from "react"
4
- import { useMediaQuery } from "../../hooks/use-media-query"
5
- import { cn } from "../../lib/class-names"
3
+ import { type CSSProperties, useEffect, useRef, useState } from "react";
4
+ import { useMediaQuery } from "../../hooks/use-media-query";
5
+ import { cn } from "../../lib/class-names";
6
6
 
7
7
  /* ══ Wake slider ══════════════════════════════════════════
8
8
  A range with no thumb: a row of thin bars, solid up to the
@@ -24,222 +24,237 @@ import { cn } from "../../lib/class-names"
24
24
  changes, which is what the reading and the colours need, and
25
25
  the loop runs only while some bar still has energy. */
26
26
 
27
- const H: Record<WakeSliderSize, number> = { sm: 16, md: 20, lg: 26 } // a bar at rest, px
27
+ const H: Record<WakeSliderSize, number> = { sm: 16, md: 20, lg: 26 }; // a bar at rest, px
28
28
 
29
- export type WakeSliderSize = "sm" | "md" | "lg"
29
+ export type WakeSliderSize = "sm" | "md" | "lg";
30
30
 
31
31
  export interface WakeSliderProps {
32
- label: string
33
- value?: number
34
- defaultValue?: number
35
- onChange?: (value: number) => void
36
- bars?: number
37
- wake?: number
38
- disabled?: boolean
39
- className?: string
40
- formatValue?: (value: number) => string
41
- /** Bar height at rest and value text scale. @default "md" */
42
- size?: WakeSliderSize
32
+ label: string;
33
+ value?: number;
34
+ defaultValue?: number;
35
+ onChange?: (value: number) => void;
36
+ bars?: number;
37
+ wake?: number;
38
+ disabled?: boolean;
39
+ className?: string;
40
+ formatValue?: (value: number) => string;
41
+ /** Bar height at rest and value text scale. @default "md" */
42
+ size?: WakeSliderSize;
43
43
  }
44
44
 
45
45
  export function WakeSlider({
46
- label,
47
- value: controlledValue,
48
- defaultValue = 50,
49
- onChange,
50
- disabled = false,
51
- className,
52
- formatValue,
53
- size = "md",
54
- /* how many bars make the track */
55
- bars = 36,
56
- /* how high the wake rises, 0..100 */
57
- wake = 60,
46
+ label,
47
+ value: controlledValue,
48
+ defaultValue = 50,
49
+ onChange,
50
+ disabled = false,
51
+ className,
52
+ formatValue,
53
+ size = "md",
54
+ /* how many bars make the track */
55
+ bars = 36,
56
+ /* how high the wake rises, 0..100 */
57
+ wake = 60,
58
58
  }: WakeSliderProps) {
59
- const n = Math.round(clamp(Number.isFinite(bars) ? bars : 36, 12, 64))
60
- const reduced = useMediaQuery("(prefers-reduced-motion: reduce)")
61
- const [value, setValue] = useState(() => clamp(controlledValue ?? defaultValue, 0, 100))
62
- const track = useRef<HTMLDivElement | null>(null)
63
- const els = useRef<(HTMLSpanElement | null)[]>([])
64
- const st = useRef({
65
- energy: new Float32Array(n),
66
- down: false,
67
- lastX: 0,
68
- lastT: 0,
69
- speed: 0,
70
- raf: 0,
71
- at: value / 100, // the value as a fraction, without React in the way
72
- lastBar: -1,
73
- })
74
- const knob = useRef(wake)
75
- knob.current = reduced ? 0 : clamp(wake, 0, 100)
76
- useEffect(() => {
77
- if (controlledValue === undefined) return
78
- const next = clamp(controlledValue, 0, 100)
79
- st.current.at = next / 100
80
- setValue(next)
81
- }, [controlledValue])
59
+ const n = Math.round(clamp(Number.isFinite(bars) ? bars : 36, 12, 64));
60
+ const reduced = useMediaQuery("(prefers-reduced-motion: reduce)");
61
+ const [value, setValue] = useState(() =>
62
+ clamp(controlledValue ?? defaultValue, 0, 100),
63
+ );
64
+ const track = useRef<HTMLDivElement | null>(null);
65
+ const els = useRef<(HTMLSpanElement | null)[]>([]);
66
+ const st = useRef({
67
+ energy: new Float32Array(n),
68
+ down: false,
69
+ lastX: 0,
70
+ lastT: 0,
71
+ speed: 0,
72
+ raf: 0,
73
+ at: value / 100, // the value as a fraction, without React in the way
74
+ lastBar: -1,
75
+ });
76
+ const knob = useRef(wake);
77
+ knob.current = reduced ? 0 : clamp(wake, 0, 100);
78
+ useEffect(() => {
79
+ if (controlledValue === undefined) return;
80
+ const next = clamp(controlledValue, 0, 100);
81
+ st.current.at = next / 100;
82
+ setValue(next);
83
+ }, [controlledValue]);
82
84
 
83
- useEffect(() => {
84
- if (!reduced) return
85
- cancelAnimationFrame(st.current.raf)
86
- st.current.raf = 0
87
- st.current.energy.fill(0)
88
- for (const bar of els.current) if (bar) bar.style.transform = "none"
89
- }, [reduced])
85
+ useEffect(() => {
86
+ if (!reduced) return;
87
+ cancelAnimationFrame(st.current.raf);
88
+ st.current.raf = 0;
89
+ st.current.energy.fill(0);
90
+ for (const bar of els.current) if (bar) bar.style.transform = "none";
91
+ }, [reduced]);
90
92
 
91
- /* a new bar count is a new row */
92
- useEffect(() => {
93
- st.current.energy = new Float32Array(n)
94
- }, [n])
93
+ /* a new bar count is a new row */
94
+ useEffect(() => {
95
+ st.current.energy = new Float32Array(n);
96
+ }, [n]);
95
97
 
96
- const loop = () => {
97
- const s = st.current
98
- if (s.raf) return
99
- let prev = performance.now()
100
- const run = (now: number) => {
101
- const dt = Math.min(0.05, (now - prev) / 1000)
102
- prev = now
103
- let live = false
104
- const fall = Math.exp(-dt * 6.5)
105
- for (let i = 0; i < s.energy.length; i++) {
106
- s.energy[i] *= fall
107
- if (s.energy[i] < 0.002) s.energy[i] = 0
108
- else live = true
109
- const b = els.current[i]
110
- /* up to 2.6 times as tall at the very top of the knob */
111
- if (b) b.style.transform = `scaleY(${(1 + s.energy[i] * 1.6).toFixed(3)})`
112
- }
113
- s.raf = live ? requestAnimationFrame(run) : 0
114
- }
115
- s.raf = requestAnimationFrame(run)
116
- }
98
+ const loop = () => {
99
+ const s = st.current;
100
+ if (s.raf) return;
101
+ let prev = performance.now();
102
+ const run = (now: number) => {
103
+ const dt = Math.min(0.05, (now - prev) / 1000);
104
+ prev = now;
105
+ let live = false;
106
+ const fall = Math.exp(-dt * 6.5);
107
+ for (let i = 0; i < s.energy.length; i++) {
108
+ s.energy[i] *= fall;
109
+ if (s.energy[i] < 0.002) s.energy[i] = 0;
110
+ else live = true;
111
+ const b = els.current[i];
112
+ /* up to 2.6 times as tall at the very top of the knob */
113
+ if (b)
114
+ b.style.transform = `scaleY(${(1 + s.energy[i] * 1.6).toFixed(3)})`;
115
+ }
116
+ s.raf = live ? requestAnimationFrame(run) : 0;
117
+ };
118
+ s.raf = requestAnimationFrame(run);
119
+ };
117
120
 
118
- useEffect(() => () => cancelAnimationFrame(st.current.raf), [])
121
+ useEffect(() => () => cancelAnimationFrame(st.current.raf), []);
119
122
 
120
- /* move the value to a fraction, waking every bar it passes */
121
- const to = (f: number, speed: number) => {
122
- const s = st.current
123
- f = clamp(f, 0, 1)
124
- const from = s.at
125
- s.at = f
126
- const a = Math.round(from * (n - 1)),
127
- b = Math.round(f * (n - 1))
128
- /* how hard: the drag's speed in bars per second, through a
123
+ /* move the value to a fraction, waking every bar it passes */
124
+ const to = (f: number, speed: number) => {
125
+ const s = st.current;
126
+ f = clamp(f, 0, 1);
127
+ const from = s.at;
128
+ s.at = f;
129
+ const a = Math.round(from * (n - 1));
130
+ const b = Math.round(f * (n - 1));
131
+ /* how hard: the drag's speed in bars per second, through a
129
132
  curve that saturates, scaled by the knob */
130
- const push = (1 - Math.exp(-speed / 900)) * (knob.current / 60)
131
- const lo = Math.min(a, b),
132
- hi = Math.max(a, b)
133
- for (let i = lo; i <= hi; i++) s.energy[i] = Math.max(s.energy[i], push)
134
- if (b !== s.lastBar) {
135
- s.lastBar = b
136
- }
137
- const next = Math.round(f * 100)
138
- setValue(next)
139
- onChange?.(next)
140
- if (!reduced && knob.current > 0) loop()
141
- }
133
+ const push = (1 - Math.exp(-speed / 900)) * (knob.current / 60);
134
+ const lo = Math.min(a, b);
135
+ const hi = Math.max(a, b);
136
+ for (let i = lo; i <= hi; i++) s.energy[i] = Math.max(s.energy[i], push);
137
+ if (b !== s.lastBar) {
138
+ s.lastBar = b;
139
+ }
140
+ const next = Math.round(f * 100);
141
+ setValue(next);
142
+ onChange?.(next);
143
+ if (!reduced && knob.current > 0) loop();
144
+ };
142
145
 
143
- const frac = (clientX: number) => {
144
- const r = track.current!.getBoundingClientRect()
145
- const fraction = r.width ? (clientX - r.left) / r.width : 0
146
- return getComputedStyle(track.current!).direction === "rtl" ? 1 - fraction : fraction
147
- }
146
+ const frac = (clientX: number) => {
147
+ const r = (track.current ?? document.body).getBoundingClientRect();
148
+ const fraction = r.width ? (clientX - r.left) / r.width : 0;
149
+ return getComputedStyle(track.current ?? document.body).direction === "rtl"
150
+ ? 1 - fraction
151
+ : fraction;
152
+ };
148
153
 
149
- const lit = Math.round((value / 100) * (n - 1))
154
+ const lit = Math.round((value / 100) * (n - 1));
150
155
 
151
- return (
152
- <div
153
- className={cn("passui-wake-slider not-typeset", disabled && "opacity-40", className)}
154
- data-size={size}
155
- >
156
- <div
157
- ref={track}
158
- className="passui-wake-track"
159
- role="slider"
160
- tabIndex={disabled ? -1 : 0}
161
- aria-label={label}
162
- aria-disabled={disabled || undefined}
163
- aria-valuetext={formatValue?.(value)}
164
- aria-valuemin={0}
165
- aria-valuemax={100}
166
- aria-valuenow={value}
167
- style={{ height: H[size] * 2.6, "--wake-bars": n } as CSSProperties}
168
- onPointerDown={(e) => {
169
- if (disabled || e.button !== 0) return
170
- e.currentTarget.focus()
171
- const s = st.current
172
- try {
173
- e.currentTarget.setPointerCapture(e.pointerId)
174
- } catch {
175
- /* a scripted pointer */
176
- }
177
- s.down = true
178
- s.lastX = e.clientX
179
- s.lastT = performance.now()
180
- s.speed = 0
181
- to(frac(e.clientX), 0)
182
- }}
183
- onPointerMove={(e) => {
184
- const s = st.current
185
- if (!s.down || disabled) return
186
- const now = performance.now()
187
- const dt = Math.max(0.008, (now - s.lastT) / 1000)
188
- /* speed in px per second, smoothed so one jittery frame
156
+ return (
157
+ <div
158
+ className={cn(
159
+ "passui-wake-slider not-typeset",
160
+ disabled && "opacity-40",
161
+ className,
162
+ )}
163
+ data-size={size}
164
+ >
165
+ <div
166
+ ref={track}
167
+ className="passui-wake-track"
168
+ role="slider"
169
+ tabIndex={disabled ? -1 : 0}
170
+ aria-label={label}
171
+ aria-disabled={disabled || undefined}
172
+ aria-valuetext={formatValue?.(value)}
173
+ aria-valuemin={0}
174
+ aria-valuemax={100}
175
+ aria-valuenow={value}
176
+ style={{ height: H[size] * 2.6, "--wake-bars": n } as CSSProperties}
177
+ onPointerDown={(e) => {
178
+ if (disabled || e.button !== 0) return;
179
+ e.currentTarget.focus();
180
+ const s = st.current;
181
+ try {
182
+ e.currentTarget.setPointerCapture(e.pointerId);
183
+ } catch {
184
+ /* a scripted pointer */
185
+ }
186
+ s.down = true;
187
+ s.lastX = e.clientX;
188
+ s.lastT = performance.now();
189
+ s.speed = 0;
190
+ to(frac(e.clientX), 0);
191
+ }}
192
+ onPointerMove={(e) => {
193
+ const s = st.current;
194
+ if (!s.down || disabled) return;
195
+ const now = performance.now();
196
+ const dt = Math.max(0.008, (now - s.lastT) / 1000);
197
+ /* speed in px per second, smoothed so one jittery frame
189
198
  cannot fake a fling */
190
- const v = Math.abs(e.clientX - s.lastX) / dt
191
- s.speed = s.speed * 0.5 + v * 0.5
192
- s.lastX = e.clientX
193
- s.lastT = now
194
- to(frac(e.clientX), s.speed)
195
- }}
196
- onPointerUp={(e) => {
197
- st.current.down = false
198
- try {
199
- e.currentTarget.releasePointerCapture(e.pointerId)
200
- } catch {
201
- /* never captured */
202
- }
203
- }}
204
- onLostPointerCapture={() => {
205
- st.current.down = false
206
- }}
207
- onPointerCancel={() => {
208
- st.current.down = false
209
- }}
210
- onKeyDown={(e) => {
211
- if (disabled) return
212
- const step = e.shiftKey ? 10 : 1
213
- let next: number | null = null
214
- const rtl = getComputedStyle(e.currentTarget).direction === "rtl"
215
- if (e.key === "ArrowUp" || e.key === (rtl ? "ArrowLeft" : "ArrowRight"))
216
- next = value + step
217
- if (e.key === "ArrowDown" || e.key === (rtl ? "ArrowRight" : "ArrowLeft"))
218
- next = value - step
219
- if (e.key === "Home") next = 0
220
- if (e.key === "End") next = 100
221
- if (next === null) return
222
- e.preventDefault()
223
- /* a key press is a small, quick step: a little wake */
224
- to(clamp(next, 0, 100) / 100, 600)
225
- }}
226
- >
227
- {Array.from({ length: n }, (_, i) => (
228
- <span
229
- key={i}
230
- ref={(b) => {
231
- els.current[i] = b
232
- }}
233
- aria-hidden="true"
234
- className="passui-wake-bar"
235
- data-on={(value > 0 && i <= lit) || undefined}
236
- style={{ height: H[size] }}
237
- />
238
- ))}
239
- </div>
240
- <span aria-hidden="true" className="passui-wake-value">
241
- {formatValue?.(value) ?? value}
242
- </span>
243
- </div>
244
- )
199
+ const v = Math.abs(e.clientX - s.lastX) / dt;
200
+ s.speed = s.speed * 0.5 + v * 0.5;
201
+ s.lastX = e.clientX;
202
+ s.lastT = now;
203
+ to(frac(e.clientX), s.speed);
204
+ }}
205
+ onPointerUp={(e) => {
206
+ st.current.down = false;
207
+ try {
208
+ e.currentTarget.releasePointerCapture(e.pointerId);
209
+ } catch {
210
+ /* never captured */
211
+ }
212
+ }}
213
+ onLostPointerCapture={() => {
214
+ st.current.down = false;
215
+ }}
216
+ onPointerCancel={() => {
217
+ st.current.down = false;
218
+ }}
219
+ onKeyDown={(e) => {
220
+ if (disabled) return;
221
+ const step = e.shiftKey ? 10 : 1;
222
+ let next: number | null = null;
223
+ const rtl = getComputedStyle(e.currentTarget).direction === "rtl";
224
+ if (
225
+ e.key === "ArrowUp" ||
226
+ e.key === (rtl ? "ArrowLeft" : "ArrowRight")
227
+ )
228
+ next = value + step;
229
+ if (
230
+ e.key === "ArrowDown" ||
231
+ e.key === (rtl ? "ArrowRight" : "ArrowLeft")
232
+ )
233
+ next = value - step;
234
+ if (e.key === "Home") next = 0;
235
+ if (e.key === "End") next = 100;
236
+ if (next === null) return;
237
+ e.preventDefault();
238
+ /* a key press is a small, quick step: a little wake */
239
+ to(clamp(next, 0, 100) / 100, 600);
240
+ }}
241
+ >
242
+ {Array.from({ length: n }, (_, i) => (
243
+ <span
244
+ key={i}
245
+ ref={(b) => {
246
+ els.current[i] = b;
247
+ }}
248
+ aria-hidden="true"
249
+ className="passui-wake-bar"
250
+ data-on={(value > 0 && i <= lit) || undefined}
251
+ style={{ height: H[size] }}
252
+ />
253
+ ))}
254
+ </div>
255
+ <span aria-hidden="true" className="passui-wake-value">
256
+ {formatValue?.(value) ?? value}
257
+ </span>
258
+ </div>
259
+ );
245
260
  }
@@ -1,9 +1,13 @@
1
1
  /** Programmatic restoration does not collapse chrome; reaching the top always restores it. */
2
- export function minimizeDecision(top: number, anchor: number, userDriven: boolean): boolean | null {
3
- if (top < 24) return false
4
- if (!userDriven) return null
5
- const delta = top - anchor
6
- if (delta < -12) return false
7
- if (delta > 12) return true
8
- return null
2
+ export function minimizeDecision(
3
+ top: number,
4
+ anchor: number,
5
+ userDriven: boolean,
6
+ ): boolean | null {
7
+ if (top < 24) return false;
8
+ if (!userDriven) return null;
9
+ const delta = top - anchor;
10
+ if (delta < -12) return false;
11
+ if (delta > 12) return true;
12
+ return null;
9
13
  }
@@ -1,23 +1,23 @@
1
- import { useCallback, useEffect, useState } from "react"
1
+ import { useCallback, useEffect, useState } from "react";
2
2
 
3
3
  /** Keep the panel mounted until its material has returned to the trigger. */
4
4
  export function useAnchoredDisclosure(disabled = false) {
5
- const [open, setOpen] = useState(false)
6
- const [mounted, setMounted] = useState(false)
7
- useEffect(() => {
8
- if (disabled) setOpen(false)
9
- }, [disabled])
10
- const show = useCallback(() => {
11
- setMounted(true)
12
- setOpen(true)
13
- }, [])
14
- const toggle = useCallback(() => {
15
- setMounted(true)
16
- setOpen((current) => !current)
17
- }, [])
18
- const close = useCallback(() => setOpen(false), [])
19
- const onExited = useCallback(() => {
20
- if (!open || disabled) setMounted(false)
21
- }, [open, disabled])
22
- return { open: open && !disabled, mounted, show, toggle, close, onExited }
5
+ const [open, setOpen] = useState(false);
6
+ const [mounted, setMounted] = useState(false);
7
+ useEffect(() => {
8
+ if (disabled) setOpen(false);
9
+ }, [disabled]);
10
+ const show = useCallback(() => {
11
+ setMounted(true);
12
+ setOpen(true);
13
+ }, []);
14
+ const toggle = useCallback(() => {
15
+ setMounted(true);
16
+ setOpen((current) => !current);
17
+ }, []);
18
+ const close = useCallback(() => setOpen(false), []);
19
+ const onExited = useCallback(() => {
20
+ if (!open || disabled) setMounted(false);
21
+ }, [open, disabled]);
22
+ return { open: open && !disabled, mounted, show, toggle, close, onExited };
23
23
  }
@@ -1,5 +1,8 @@
1
- import { useLayoutEffect, useState } from "react"
2
- import { type AnchoredPlacement, placeAnchored } from "../lib/anchored-placement"
1
+ import { useLayoutEffect, useState } from "react";
2
+ import {
3
+ type AnchoredPlacement,
4
+ placeAnchored,
5
+ } from "../lib/anchored-placement";
3
6
 
4
7
  /**
5
8
  * Places a panel of `width` and `needed` height under its anchor and
@@ -7,30 +10,31 @@ import { type AnchoredPlacement, placeAnchored } from "../lib/anchored-placement
7
10
  * scrolls, so it never hangs off the screen.
8
11
  */
9
12
  export function useAnchoredPlacement(
10
- anchor: HTMLElement,
11
- width: number,
12
- needed: number,
13
- overlap = false
13
+ anchor: HTMLElement,
14
+ width: number,
15
+ needed: number,
16
+ overlap = false,
14
17
  ): AnchoredPlacement {
15
- const [placement, setPlacement] = useState(() =>
16
- placeAnchored(anchor, width, needed, 6, overlap)
17
- )
18
+ const [placement, setPlacement] = useState(() =>
19
+ placeAnchored(anchor, width, needed, 6, overlap),
20
+ );
18
21
 
19
- useLayoutEffect(() => {
20
- const replace = () => setPlacement(placeAnchored(anchor, width, needed, 6, overlap))
21
- replace()
22
- const viewport = window.visualViewport
23
- viewport?.addEventListener("resize", replace)
24
- viewport?.addEventListener("scroll", replace)
25
- window.addEventListener("resize", replace)
26
- window.addEventListener("scroll", replace, true)
27
- return () => {
28
- viewport?.removeEventListener("resize", replace)
29
- viewport?.removeEventListener("scroll", replace)
30
- window.removeEventListener("resize", replace)
31
- window.removeEventListener("scroll", replace, true)
32
- }
33
- }, [anchor, width, needed, overlap])
22
+ useLayoutEffect(() => {
23
+ const replace = () =>
24
+ setPlacement(placeAnchored(anchor, width, needed, 6, overlap));
25
+ replace();
26
+ const viewport = window.visualViewport;
27
+ viewport?.addEventListener("resize", replace);
28
+ viewport?.addEventListener("scroll", replace);
29
+ window.addEventListener("resize", replace);
30
+ window.addEventListener("scroll", replace, true);
31
+ return () => {
32
+ viewport?.removeEventListener("resize", replace);
33
+ viewport?.removeEventListener("scroll", replace);
34
+ window.removeEventListener("resize", replace);
35
+ window.removeEventListener("scroll", replace, true);
36
+ };
37
+ }, [anchor, width, needed, overlap]);
34
38
 
35
- return placement
39
+ return placement;
36
40
  }