@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,38 +1,38 @@
1
- import { useAnchoredDisclosure } from "../../hooks/use-anchored-disclosure"
2
- import { AnchoredSurface } from "../popover/anchored-surface"
3
- import { type KeyboardEvent, useEffect, useId, useRef } from "react"
4
- import { createPortal } from "react-dom"
5
- import { ClockIcon } from "../../icons/icons"
6
- import { cn } from "../../lib/class-names"
7
- import { haptic } from "../../lib/haptics"
8
- import { pickerTriggerStyle, type PickerSize } from "../picker/picker-trigger"
1
+ import { useAnchoredDisclosure } from "../../hooks/use-anchored-disclosure";
2
+ import { AnchoredSurface } from "../popover/anchored-surface";
3
+ import { type KeyboardEvent, useEffect, useId, useRef } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { ClockIcon } from "../../icons/icons";
6
+ import { cn } from "../../lib/class-names";
7
+ import { haptic } from "../../lib/haptics";
8
+ import { pickerTriggerStyle, type PickerSize } from "../picker/picker-trigger";
9
9
  import {
10
- formatTime,
11
- fromTwelveHours,
12
- minuteChoices,
13
- parseTime,
14
- type TimeOfDay,
15
- toTwelveHours,
16
- usesTwelveHours,
17
- } from "./time-value"
10
+ formatTime,
11
+ fromTwelveHours,
12
+ minuteChoices,
13
+ parseTime,
14
+ type TimeOfDay,
15
+ toTwelveHours,
16
+ usesTwelveHours,
17
+ } from "./time-value";
18
18
 
19
19
  export interface CompactTimePickerProps {
20
- /** `HH:MM` on a 24 hour clock, or "" when no time is chosen. */
21
- value: string
22
- onChange: (value: string) => void
23
- /** Names the button and the panel for assistive technology. */
24
- label: string
25
- locale: string
26
- /** Minutes offered, in steps; an exact time already chosen is kept. */
27
- minuteStep?: number
28
- /** Shown when no time is chosen. */
29
- placeholder?: string
30
- labels: { hour: string; minute: string; period: string }
31
- className?: string
32
- /** Prevents opening the time panel. @default false */
33
- disabled?: boolean
34
- /** Trigger button density; matches Picker's sm | md | lg. @default "md" */
35
- size?: PickerSize
20
+ /** `HH:MM` on a 24 hour clock, or "" when no time is chosen. */
21
+ value: string;
22
+ onChange: (value: string) => void;
23
+ /** Names the button and the panel for assistive technology. */
24
+ label: string;
25
+ locale: string;
26
+ /** Minutes offered, in steps; an exact time already chosen is kept. */
27
+ minuteStep?: number;
28
+ /** Shown when no time is chosen. */
29
+ placeholder?: string;
30
+ labels: { hour: string; minute: string; period: string };
31
+ className?: string;
32
+ /** Prevents opening the time panel. @default false */
33
+ disabled?: boolean;
34
+ /** Trigger button density; matches Picker's sm | md | lg. @default "md" */
35
+ size?: PickerSize;
36
36
  }
37
37
 
38
38
  /**
@@ -41,274 +41,281 @@ export interface CompactTimePickerProps {
41
41
  * reads (12 hours with AM and PM, or 24).
42
42
  */
43
43
  export function CompactTimePicker({
44
- value,
45
- onChange,
46
- label,
47
- locale,
48
- minuteStep = 5,
49
- placeholder,
50
- labels,
51
- className,
52
- disabled = false,
53
- size = "md",
44
+ value,
45
+ onChange,
46
+ label,
47
+ locale,
48
+ minuteStep = 5,
49
+ placeholder,
50
+ labels,
51
+ className,
52
+ disabled = false,
53
+ size = "md",
54
54
  }: CompactTimePickerProps) {
55
- const panel = useAnchoredDisclosure(disabled)
56
- const { open } = panel
57
- const trigger = useRef<HTMLButtonElement>(null)
58
- const panelId = useId()
59
- const chosen = parseTime(value)
60
- const shown = chosen
61
- ? new Intl.DateTimeFormat(locale, {
62
- hour: "numeric",
63
- minute: "2-digit",
64
- timeZone: "UTC",
65
- }).format(new Date(Date.UTC(2000, 0, 1, chosen.hour, chosen.minute)))
66
- : (placeholder ?? "")
55
+ const panel = useAnchoredDisclosure(disabled);
56
+ const { open } = panel;
57
+ const trigger = useRef<HTMLButtonElement>(null);
58
+ const panelId = useId();
59
+ const chosen = parseTime(value);
60
+ const shown = chosen
61
+ ? new Intl.DateTimeFormat(locale, {
62
+ hour: "numeric",
63
+ minute: "2-digit",
64
+ timeZone: "UTC",
65
+ }).format(new Date(Date.UTC(2000, 0, 1, chosen.hour, chosen.minute)))
66
+ : (placeholder ?? "");
67
67
 
68
- return (
69
- <>
70
- <button
71
- ref={trigger}
72
- type="button"
73
- aria-label={chosen ? `${label}, ${shown}` : label}
74
- aria-haspopup="dialog"
75
- aria-expanded={open && !disabled}
76
- aria-controls={open && !disabled ? panelId : undefined}
77
- disabled={disabled}
78
- onClick={() => panel.toggle()}
79
- className={pickerTriggerStyle(className, size)}
80
- >
81
- <span
82
- className={cn(
83
- "min-w-0 grow truncate tabular-nums",
84
- !chosen && "text-label-tertiary"
85
- )}
86
- >
87
- {shown}
88
- </span>
89
- <ClockIcon size={16} className="shrink-0 text-label-tertiary" />
90
- </button>
91
- {typeof document !== "undefined" &&
92
- createPortal(
93
- panel.mounted && trigger.current ? (
94
- <TimeColumns
95
- open={open}
96
- onExited={panel.onExited}
97
- id={panelId}
98
- anchor={trigger.current}
99
- label={label}
100
- locale={locale}
101
- time={
102
- chosen ?? {
103
- hour: 9,
104
- minute: 0,
105
- }
106
- }
107
- minuteStep={minuteStep}
108
- labels={labels}
109
- onChange={(next) => {
110
- haptic("selection")
111
- onChange(formatTime(next))
112
- }}
113
- onClose={(restoreFocus) => {
114
- panel.close()
115
- if (restoreFocus) trigger.current?.focus({ preventScroll: true })
116
- }}
117
- />
118
- ) : null,
119
- document.body
120
- )}
121
- </>
122
- )
68
+ return (
69
+ <>
70
+ <button
71
+ ref={trigger}
72
+ type="button"
73
+ aria-label={chosen ? `${label}, ${shown}` : label}
74
+ aria-haspopup="dialog"
75
+ aria-expanded={open && !disabled}
76
+ aria-controls={open && !disabled ? panelId : undefined}
77
+ disabled={disabled}
78
+ onClick={() => panel.toggle()}
79
+ className={pickerTriggerStyle(className, size)}
80
+ >
81
+ <span
82
+ className={cn(
83
+ "min-w-0 grow truncate tabular-nums",
84
+ !chosen && "text-label-tertiary",
85
+ )}
86
+ >
87
+ {shown}
88
+ </span>
89
+ <ClockIcon size={16} className="text-label-tertiary shrink-0" />
90
+ </button>
91
+ {typeof document !== "undefined" &&
92
+ createPortal(
93
+ panel.mounted && trigger.current ? (
94
+ <TimeColumns
95
+ open={open}
96
+ onExited={panel.onExited}
97
+ id={panelId}
98
+ anchor={trigger.current}
99
+ label={label}
100
+ locale={locale}
101
+ time={
102
+ chosen ?? {
103
+ hour: 9,
104
+ minute: 0,
105
+ }
106
+ }
107
+ minuteStep={minuteStep}
108
+ labels={labels}
109
+ onChange={(next) => {
110
+ haptic("selection");
111
+ onChange(formatTime(next));
112
+ }}
113
+ onClose={(restoreFocus) => {
114
+ panel.close();
115
+ if (restoreFocus)
116
+ trigger.current?.focus({ preventScroll: true });
117
+ }}
118
+ />
119
+ ) : null,
120
+ document.body,
121
+ )}
122
+ </>
123
+ );
123
124
  }
124
125
 
125
126
  interface ColumnItem {
126
- key: number
127
- text: string
127
+ key: number;
128
+ text: string;
128
129
  }
129
130
 
130
131
  function TimeColumns({
131
- open,
132
- onExited,
133
- id,
134
- anchor,
135
- label,
136
- locale,
137
- time,
138
- minuteStep,
139
- labels,
140
- onChange,
141
- onClose,
132
+ open,
133
+ onExited,
134
+ id,
135
+ anchor,
136
+ label,
137
+ locale,
138
+ time,
139
+ minuteStep,
140
+ labels,
141
+ onChange,
142
+ onClose,
142
143
  }: {
143
- open: boolean
144
- onExited: () => void
145
- id: string
146
- anchor: HTMLElement
147
- label: string
148
- locale: string
149
- time: TimeOfDay
150
- minuteStep: number
151
- labels: CompactTimePickerProps["labels"]
152
- onChange: (time: TimeOfDay) => void
153
- onClose: (restoreFocus: boolean) => void
144
+ open: boolean;
145
+ onExited: () => void;
146
+ id: string;
147
+ anchor: HTMLElement;
148
+ label: string;
149
+ locale: string;
150
+ time: TimeOfDay;
151
+ minuteStep: number;
152
+ labels: CompactTimePickerProps["labels"];
153
+ onChange: (time: TimeOfDay) => void;
154
+ onClose: (restoreFocus: boolean) => void;
154
155
  }) {
155
- const root = useRef<HTMLDivElement>(null)
156
- const twelve = usesTwelveHours(locale)
157
- const number = new Intl.NumberFormat(locale, {
158
- minimumIntegerDigits: 2,
159
- })
160
- const plain = new Intl.NumberFormat(locale)
161
- const clock = toTwelveHours(time.hour)
156
+ const root = useRef<HTMLDivElement>(null);
157
+ const twelve = usesTwelveHours(locale);
158
+ const number = new Intl.NumberFormat(locale, {
159
+ minimumIntegerDigits: 2,
160
+ });
161
+ const plain = new Intl.NumberFormat(locale);
162
+ const clock = toTwelveHours(time.hour);
162
163
 
163
- const hours: ColumnItem[] = twelve
164
- ? [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((hour) => ({
165
- key: hour,
166
- text: plain.format(hour),
167
- }))
168
- : Array.from({ length: 24 }, (_, hour) => ({
169
- key: hour,
170
- text: number.format(hour),
171
- }))
172
- const minutes = minuteChoices(minuteStep, time.minute).map((minute) => ({
173
- key: minute,
174
- text: number.format(minute),
175
- }))
176
- // The locale's own words for morning and afternoon.
177
- const periodName = (pm: boolean) =>
178
- new Intl.DateTimeFormat(locale, {
179
- hour: "numeric",
180
- hour12: true,
181
- timeZone: "UTC",
182
- })
183
- .formatToParts(new Date(Date.UTC(2000, 0, 1, pm ? 13 : 1)))
184
- .find((part) => part.type === "dayPeriod")?.value ?? (pm ? "PM" : "AM")
185
- const periods = [
186
- { key: 0, text: periodName(false) },
187
- { key: 1, text: periodName(true) },
188
- ]
164
+ const hours: ColumnItem[] = twelve
165
+ ? [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((hour) => ({
166
+ key: hour,
167
+ text: plain.format(hour),
168
+ }))
169
+ : Array.from({ length: 24 }, (_, hour) => ({
170
+ key: hour,
171
+ text: number.format(hour),
172
+ }));
173
+ const minutes = minuteChoices(minuteStep, time.minute).map((minute) => ({
174
+ key: minute,
175
+ text: number.format(minute),
176
+ }));
177
+ // The locale's own words for morning and afternoon.
178
+ const periodName = (pm: boolean) =>
179
+ new Intl.DateTimeFormat(locale, {
180
+ hour: "numeric",
181
+ hour12: true,
182
+ timeZone: "UTC",
183
+ })
184
+ .formatToParts(new Date(Date.UTC(2000, 0, 1, pm ? 13 : 1)))
185
+ .find((part) => part.type === "dayPeriod")?.value ?? (pm ? "PM" : "AM");
186
+ const periods = [
187
+ { key: 0, text: periodName(false) },
188
+ { key: 1, text: periodName(true) },
189
+ ];
189
190
 
190
- return (
191
- <AnchoredSurface
192
- open={open}
193
- onExited={onExited}
194
- ref={root}
195
- anchor={anchor}
196
- width={twelve ? 240 : 180}
197
- needed={280}
198
- onClose={onClose}
199
- id={id}
200
- role="dialog"
201
- aria-label={label}
202
- contentClassName="flex-row gap-1 overflow-hidden p-2"
203
- onKeyDown={(event) => {
204
- if (event.key !== "Escape") return
205
- event.preventDefault()
206
- event.stopPropagation()
207
- onClose(true)
208
- }}
209
- >
210
- <Column
211
- label={labels.hour}
212
- items={hours}
213
- selected={twelve ? clock.hour : time.hour}
214
- autoFocus
215
- onSelect={(hour) =>
216
- onChange({
217
- ...time,
218
- hour: twelve ? fromTwelveHours(hour, clock.pm) : hour,
219
- })
220
- }
221
- />
222
- <Column
223
- label={labels.minute}
224
- items={minutes}
225
- selected={time.minute}
226
- onSelect={(minute) => onChange({ ...time, minute })}
227
- />
228
- {twelve && (
229
- <Column
230
- label={labels.period}
231
- items={periods}
232
- selected={clock.pm ? 1 : 0}
233
- onSelect={(period) =>
234
- onChange({
235
- ...time,
236
- hour: fromTwelveHours(clock.hour, period === 1),
237
- })
238
- }
239
- />
240
- )}
241
- </AnchoredSurface>
242
- )
191
+ return (
192
+ <AnchoredSurface
193
+ open={open}
194
+ onExited={onExited}
195
+ ref={root}
196
+ anchor={anchor}
197
+ width={twelve ? 240 : 180}
198
+ needed={280}
199
+ onClose={onClose}
200
+ id={id}
201
+ role="dialog"
202
+ aria-label={label}
203
+ contentClassName="flex-row gap-1 overflow-hidden p-2"
204
+ onKeyDown={(event) => {
205
+ if (event.key !== "Escape") return;
206
+ event.preventDefault();
207
+ event.stopPropagation();
208
+ onClose(true);
209
+ }}
210
+ >
211
+ <Column
212
+ label={labels.hour}
213
+ items={hours}
214
+ selected={twelve ? clock.hour : time.hour}
215
+ autoFocus
216
+ onSelect={(hour) =>
217
+ onChange({
218
+ ...time,
219
+ hour: twelve ? fromTwelveHours(hour, clock.pm) : hour,
220
+ })
221
+ }
222
+ />
223
+ <Column
224
+ label={labels.minute}
225
+ items={minutes}
226
+ selected={time.minute}
227
+ onSelect={(minute) => onChange({ ...time, minute })}
228
+ />
229
+ {twelve && (
230
+ <Column
231
+ label={labels.period}
232
+ items={periods}
233
+ selected={clock.pm ? 1 : 0}
234
+ onSelect={(period) =>
235
+ onChange({
236
+ ...time,
237
+ hour: fromTwelveHours(clock.hour, period === 1),
238
+ })
239
+ }
240
+ />
241
+ )}
242
+ </AnchoredSurface>
243
+ );
243
244
  }
244
245
 
245
246
  /** One scrolling column of choices; Up and Down move through it. */
246
247
  function Column({
247
- label,
248
- items,
249
- selected,
250
- autoFocus = false,
251
- onSelect,
248
+ label,
249
+ items,
250
+ selected,
251
+ autoFocus = false,
252
+ onSelect,
252
253
  }: {
253
- label: string
254
- items: ColumnItem[]
255
- selected: number
256
- autoFocus?: boolean
257
- onSelect: (key: number) => void
254
+ label: string;
255
+ items: ColumnItem[];
256
+ selected: number;
257
+ autoFocus?: boolean;
258
+ onSelect: (key: number) => void;
258
259
  }) {
259
- const list = useRef<HTMLDivElement>(null)
260
+ const list = useRef<HTMLDivElement>(null);
260
261
 
261
- // The chosen item starts in the middle of the column, then follows changes.
262
- useEffect(() => {
263
- const element = list.current?.querySelector<HTMLElement>(`[data-key="${selected}"]`)
264
- element?.scrollIntoView({ block: "center" })
265
- if (autoFocus && list.current?.contains(document.activeElement) === false) element?.focus()
266
- }, [selected, autoFocus])
262
+ // The chosen item starts in the middle of the column, then follows changes.
263
+ useEffect(() => {
264
+ const element = list.current?.querySelector<HTMLElement>(
265
+ `[data-key="${selected}"]`,
266
+ );
267
+ element?.scrollIntoView({ block: "center" });
268
+ if (autoFocus && list.current?.contains(document.activeElement) === false)
269
+ element?.focus();
270
+ }, [selected, autoFocus]);
267
271
 
268
- const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
269
- const step = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0
270
- if (!step) return
271
- event.preventDefault()
272
- const index = items.findIndex((item) => item.key === selected)
273
- const next = items[(index + step + items.length) % items.length]
274
- if (!next) return
275
- onSelect(next.key)
276
- requestAnimationFrame(() =>
277
- list.current?.querySelector<HTMLElement>(`[data-key="${next.key}"]`)?.focus()
278
- )
279
- }
272
+ const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
273
+ const step =
274
+ event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
275
+ if (!step) return;
276
+ event.preventDefault();
277
+ const index = items.findIndex((item) => item.key === selected);
278
+ const next = items[(index + step + items.length) % items.length];
279
+ if (!next) return;
280
+ onSelect(next.key);
281
+ requestAnimationFrame(() =>
282
+ list.current
283
+ ?.querySelector<HTMLElement>(`[data-key="${next.key}"]`)
284
+ ?.focus(),
285
+ );
286
+ };
280
287
 
281
- return (
282
- <div
283
- ref={list}
284
- role="listbox"
285
- aria-label={label}
286
- onKeyDown={onKeyDown}
287
- className="flex max-h-56 flex-1 snap-y snap-mandatory flex-col overflow-y-auto overscroll-contain py-[5.25rem] [scrollbar-width:none]"
288
- >
289
- {items.map((item) => {
290
- const isSelected = item.key === selected
291
- return (
292
- <button
293
- key={item.key}
294
- type="button"
295
- role="option"
296
- data-key={item.key}
297
- aria-selected={isSelected}
298
- tabIndex={isSelected ? 0 : -1}
299
- onClick={() => onSelect(item.key)}
300
- className={cn(
301
- "flex h-11 shrink-0 snap-center items-center justify-center rounded-xl text-title3 tabular-nums transition-colors",
302
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70",
303
- isSelected
304
- ? "bg-fill font-semibold text-label"
305
- : "text-label-secondary hover:bg-fill/60"
306
- )}
307
- >
308
- {item.text}
309
- </button>
310
- )
311
- })}
312
- </div>
313
- )
288
+ return (
289
+ <div
290
+ ref={list}
291
+ role="listbox"
292
+ aria-label={label}
293
+ onKeyDown={onKeyDown}
294
+ className="flex max-h-56 flex-1 snap-y snap-mandatory [scrollbar-width:none] flex-col overflow-y-auto overscroll-contain py-[5.25rem]"
295
+ >
296
+ {items.map((item) => {
297
+ const isSelected = item.key === selected;
298
+ return (
299
+ <button
300
+ key={item.key}
301
+ type="button"
302
+ role="option"
303
+ data-key={item.key}
304
+ aria-selected={isSelected}
305
+ tabIndex={isSelected ? 0 : -1}
306
+ onClick={() => onSelect(item.key)}
307
+ className={cn(
308
+ "text-title3 flex h-11 shrink-0 snap-center items-center justify-center rounded-xl tabular-nums transition-colors",
309
+ "focus-visible:ring-accent/70 focus-visible:ring-2 focus-visible:outline-none",
310
+ isSelected
311
+ ? "bg-fill text-label font-semibold"
312
+ : "text-label-secondary hover:bg-fill/60",
313
+ )}
314
+ >
315
+ {item.text}
316
+ </button>
317
+ );
318
+ })}
319
+ </div>
320
+ );
314
321
  }