@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,183 +1,197 @@
1
1
  import {
2
- type InputHTMLAttributes,
3
- type Ref,
4
- useEffect,
5
- useLayoutEffect,
6
- useRef,
7
- useState,
8
- } from "react"
9
- import { useMediaQuery } from "../../hooks/use-media-query"
10
- import { SearchIcon } from "../../icons/icons"
11
- import { cn } from "../../lib/class-names"
12
- import { springs } from "@passui/tokens/motion"
2
+ type InputHTMLAttributes,
3
+ type Ref,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import { useMediaQuery } from "../../hooks/use-media-query";
10
+ import { SearchIcon } from "../../icons/icons";
11
+ import { cn } from "../../lib/class-names";
12
+ import { springs } from "@passui/tokens/motion";
13
13
 
14
14
  /** One object: the field stays inside the pill, and the lens never moves within it. */
15
15
  export function ExpandingSearchField({
16
- inputRef,
17
- className,
18
- placeholder = "Search",
19
- ...props
20
- }: InputHTMLAttributes<HTMLInputElement> & { inputRef: Ref<HTMLInputElement> }) {
21
- const [expanded, setExpanded] = useState(Boolean(props.value ?? props.defaultValue))
22
- const reduced = useMediaQuery("(prefers-reduced-motion: reduce)")
23
- const frame = useRef<HTMLDivElement>(null)
24
- const pill = useRef<HTMLLabelElement>(null)
25
- const input = useRef<HTMLInputElement>(null)
26
- const geometry = useRef({
27
- width: expanded ? 320 : 44,
28
- velocity: 0,
29
- x: 0,
30
- y: 0,
31
- vx: 0,
32
- vy: 0,
33
- pullX: 0,
34
- pullY: 0,
35
- capacity: 320,
36
- raf: 0,
37
- })
38
- const current = useRef({ expanded, reduced })
39
- const wake = useRef<() => void>(() => {})
40
- useLayoutEffect(() => {
41
- const box = frame.current,
42
- label = pill.current,
43
- field = input.current
44
- if (!box || !label || !field) return
45
- const s = geometry.current
46
- let previous = 0
47
- const tick = (now: number) => {
48
- const dt = previous ? Math.min(0.032, (now - previous) / 1000) : 1 / 60
49
- previous = now
50
- const target = current.current.expanded ? s.capacity : Math.min(44, s.capacity)
51
- const pullX = current.current.expanded ? 0 : s.pullX
52
- const pullY = current.current.expanded ? 0 : s.pullY
53
- const { stiffness, damping, mass } = springs.smooth
54
- if (current.current.reduced) {
55
- s.width = target
56
- s.x = 0
57
- s.y = 0
58
- s.velocity = 0
59
- s.vx = 0
60
- s.vy = 0
61
- } else {
62
- s.velocity += (((target - s.width) * stiffness - s.velocity * damping) / mass) * dt
63
- s.width += s.velocity * dt
64
- s.vx += (((pullX - s.x) * stiffness - s.vx * damping) / mass) * dt
65
- s.vy += (((pullY - s.y) * stiffness - s.vy * damping) / mass) * dt
66
- s.x += s.vx * dt
67
- s.y += s.vy * dt
68
- }
69
- label.style.width = `${Math.max(0, s.width)}px`
70
- label.style.transform = `translate(${s.x}px, ${s.y}px)`
71
- /* One number drives the field's reveal; focus lands with the
16
+ inputRef,
17
+ className,
18
+ placeholder = "Search",
19
+ ...props
20
+ }: InputHTMLAttributes<HTMLInputElement> & {
21
+ inputRef: Ref<HTMLInputElement>;
22
+ }) {
23
+ const [expanded, setExpanded] = useState(
24
+ Boolean(props.value ?? props.defaultValue),
25
+ );
26
+ const reduced = useMediaQuery("(prefers-reduced-motion: reduce)");
27
+ const frame = useRef<HTMLDivElement>(null);
28
+ const pill = useRef<HTMLLabelElement>(null);
29
+ const input = useRef<HTMLInputElement>(null);
30
+ const geometry = useRef({
31
+ width: expanded ? 320 : 44,
32
+ velocity: 0,
33
+ x: 0,
34
+ y: 0,
35
+ vx: 0,
36
+ vy: 0,
37
+ pullX: 0,
38
+ pullY: 0,
39
+ capacity: 320,
40
+ raf: 0,
41
+ });
42
+ const current = useRef({ expanded, reduced });
43
+ const wake = useRef<() => void>(() => {});
44
+ useLayoutEffect(() => {
45
+ const box = frame.current;
46
+ const label = pill.current;
47
+ const field = input.current;
48
+ if (!box || !label || !field) return;
49
+ const s = geometry.current;
50
+ let previous = 0;
51
+ const tick = (now: number) => {
52
+ const dt = previous ? Math.min(0.032, (now - previous) / 1000) : 1 / 60;
53
+ previous = now;
54
+ const target = current.current.expanded
55
+ ? s.capacity
56
+ : Math.min(44, s.capacity);
57
+ const pullX = current.current.expanded ? 0 : s.pullX;
58
+ const pullY = current.current.expanded ? 0 : s.pullY;
59
+ const { stiffness, damping, mass } = springs.smooth;
60
+ if (current.current.reduced) {
61
+ s.width = target;
62
+ s.x = 0;
63
+ s.y = 0;
64
+ s.velocity = 0;
65
+ s.vx = 0;
66
+ s.vy = 0;
67
+ } else {
68
+ s.velocity +=
69
+ (((target - s.width) * stiffness - s.velocity * damping) / mass) * dt;
70
+ s.width += s.velocity * dt;
71
+ s.vx += (((pullX - s.x) * stiffness - s.vx * damping) / mass) * dt;
72
+ s.vy += (((pullY - s.y) * stiffness - s.vy * damping) / mass) * dt;
73
+ s.x += s.vx * dt;
74
+ s.y += s.vy * dt;
75
+ }
76
+ label.style.width = `${Math.max(0, s.width)}px`;
77
+ label.style.transform = `translate(${s.x}px, ${s.y}px)`;
78
+ /* One number drives the field's reveal; focus lands with the
72
79
  expansion, so the caret arrives while the shape is growing. */
73
- const progress = (s.width - 44) / Math.max(1, s.capacity - 44)
74
- field.style.opacity = String(Math.max(0, Math.min(1, (progress - 0.55) / 0.45)))
75
- if (
76
- Math.abs(s.width - target) < 0.05 &&
77
- Math.abs(s.velocity) < 0.05 &&
78
- Math.abs(s.x - pullX) + Math.abs(s.y - pullY) + Math.abs(s.vx) + Math.abs(s.vy) <
79
- 0.05
80
- ) {
81
- label.style.width = `${target}px`
82
- s.raf = 0
83
- previous = 0
84
- return
85
- }
86
- s.raf = requestAnimationFrame(tick)
87
- }
88
- wake.current = () => {
89
- if (!s.raf) s.raf = requestAnimationFrame(tick)
90
- }
91
- const measure = () => {
92
- s.capacity = Math.min(320, box.clientWidth)
93
- wake.current()
94
- }
95
- measure()
96
- const observer = new ResizeObserver(measure)
97
- observer.observe(box)
98
- return () => {
99
- observer.disconnect()
100
- cancelAnimationFrame(s.raf)
101
- s.raf = 0
102
- }
103
- }, [])
104
- useEffect(() => {
105
- current.current = { expanded, reduced }
106
- wake.current()
107
- }, [expanded, reduced])
108
- return (
109
- <div
110
- ref={frame}
111
- className={cn(
112
- "relative flex h-11 w-full max-w-xs items-center justify-center",
113
- className
114
- )}
115
- onPointerMove={(event) => {
116
- if (expanded || reduced || event.pointerType !== "mouse") return
117
- // Measure the frame, which stays still; measuring the object being
118
- // moved creates a feedback loop and a visible shiver.
119
- const rect = event.currentTarget.getBoundingClientRect()
120
- const dx = event.clientX - rect.left - rect.width / 2,
121
- dy = event.clientY - rect.top - rect.height / 2
122
- const distance = Math.hypot(dx, dy),
123
- pull = Math.max(0, 1 - distance / 72) ** 1.4 * 7
124
- geometry.current.pullX = distance ? (dx / distance) * pull : 0
125
- geometry.current.pullY = distance ? (dy / distance) * pull : 0
126
- wake.current()
127
- }}
128
- onPointerLeave={() => {
129
- geometry.current.pullX = 0
130
- geometry.current.pullY = 0
131
- wake.current()
132
- }}
133
- >
134
- <label
135
- ref={pill}
136
- className={cn(
137
- "passui-search-pill control-field relative block h-11 max-w-full overflow-hidden rounded-full",
138
- props.disabled && "opacity-40"
139
- )}
140
- style={{ width: expanded ? 320 : 44 }}
141
- >
142
- <SearchIcon
143
- aria-hidden="true"
144
- size={18}
145
- className="pointer-events-none absolute top-1/2 start-[13px] -translate-y-1/2 text-label-secondary"
146
- />
147
- <input
148
- {...props}
149
- ref={(node) => {
150
- input.current = node
151
- if (typeof inputRef === "function") inputRef(node)
152
- else if (inputRef) inputRef.current = node
153
- }}
154
- type="search"
155
- placeholder={placeholder}
156
- aria-label={
157
- props["aria-labelledby"] ? undefined : (props["aria-label"] ?? placeholder)
158
- }
159
- className="h-11 w-full min-w-0 bg-transparent ps-11 pe-3 text-subheadline text-label outline-none placeholder:text-label-tertiary"
160
- onFocus={(event) => {
161
- setExpanded(true)
162
- props.onFocus?.(event)
163
- }}
164
- onBlur={(event) => {
165
- if (!event.currentTarget.value) setExpanded(false)
166
- props.onBlur?.(event)
167
- }}
168
- onKeyDown={(event) => {
169
- props.onKeyDown?.(event)
170
- if (
171
- !event.defaultPrevented &&
172
- event.key === "Escape" &&
173
- !event.currentTarget.value
174
- ) {
175
- setExpanded(false)
176
- event.currentTarget.blur()
177
- }
178
- }}
179
- />
180
- </label>
181
- </div>
182
- )
80
+ const progress = (s.width - 44) / Math.max(1, s.capacity - 44);
81
+ field.style.opacity = String(
82
+ Math.max(0, Math.min(1, (progress - 0.55) / 0.45)),
83
+ );
84
+ if (
85
+ Math.abs(s.width - target) < 0.05 &&
86
+ Math.abs(s.velocity) < 0.05 &&
87
+ Math.abs(s.x - pullX) +
88
+ Math.abs(s.y - pullY) +
89
+ Math.abs(s.vx) +
90
+ Math.abs(s.vy) <
91
+ 0.05
92
+ ) {
93
+ label.style.width = `${target}px`;
94
+ s.raf = 0;
95
+ previous = 0;
96
+ return;
97
+ }
98
+ s.raf = requestAnimationFrame(tick);
99
+ };
100
+ wake.current = () => {
101
+ if (!s.raf) s.raf = requestAnimationFrame(tick);
102
+ };
103
+ const measure = () => {
104
+ s.capacity = Math.min(320, box.clientWidth);
105
+ wake.current();
106
+ };
107
+ measure();
108
+ const observer = new ResizeObserver(measure);
109
+ observer.observe(box);
110
+ return () => {
111
+ observer.disconnect();
112
+ cancelAnimationFrame(s.raf);
113
+ s.raf = 0;
114
+ };
115
+ }, []);
116
+ useEffect(() => {
117
+ current.current = { expanded, reduced };
118
+ wake.current();
119
+ }, [expanded, reduced]);
120
+ return (
121
+ <div
122
+ ref={frame}
123
+ className={cn(
124
+ "relative flex h-11 w-full max-w-xs items-center justify-center",
125
+ className,
126
+ )}
127
+ onPointerMove={(event) => {
128
+ if (expanded || reduced || event.pointerType !== "mouse") return;
129
+ // Measure the frame, which stays still; measuring the object being
130
+ // moved creates a feedback loop and a visible shiver.
131
+ const rect = event.currentTarget.getBoundingClientRect();
132
+ const dx = event.clientX - rect.left - rect.width / 2;
133
+ const dy = event.clientY - rect.top - rect.height / 2;
134
+ const distance = Math.hypot(dx, dy);
135
+ const pull = Math.max(0, 1 - distance / 72) ** 1.4 * 7;
136
+ geometry.current.pullX = distance ? (dx / distance) * pull : 0;
137
+ geometry.current.pullY = distance ? (dy / distance) * pull : 0;
138
+ wake.current();
139
+ }}
140
+ onPointerLeave={() => {
141
+ geometry.current.pullX = 0;
142
+ geometry.current.pullY = 0;
143
+ wake.current();
144
+ }}
145
+ >
146
+ <label
147
+ ref={pill}
148
+ className={cn(
149
+ "passui-search-pill control-field relative block h-11 max-w-full overflow-hidden rounded-full",
150
+ props.disabled && "opacity-40",
151
+ )}
152
+ style={{ width: expanded ? 320 : 44 }}
153
+ >
154
+ <SearchIcon
155
+ aria-hidden="true"
156
+ size={18}
157
+ className="text-label-secondary pointer-events-none absolute start-[13px] top-1/2 -translate-y-1/2"
158
+ />
159
+ <input
160
+ {...props}
161
+ ref={(node) => {
162
+ input.current = node;
163
+ if (typeof inputRef === "function") inputRef(node);
164
+ else if (inputRef) inputRef.current = node;
165
+ }}
166
+ type="search"
167
+ placeholder={placeholder}
168
+ aria-label={
169
+ props["aria-labelledby"]
170
+ ? undefined
171
+ : (props["aria-label"] ?? placeholder)
172
+ }
173
+ className="text-subheadline text-label placeholder:text-label-tertiary h-11 w-full min-w-0 bg-transparent ps-11 pe-3 outline-none"
174
+ onFocus={(event) => {
175
+ setExpanded(true);
176
+ props.onFocus?.(event);
177
+ }}
178
+ onBlur={(event) => {
179
+ if (!event.currentTarget.value) setExpanded(false);
180
+ props.onBlur?.(event);
181
+ }}
182
+ onKeyDown={(event) => {
183
+ props.onKeyDown?.(event);
184
+ if (
185
+ !event.defaultPrevented &&
186
+ event.key === "Escape" &&
187
+ !event.currentTarget.value
188
+ ) {
189
+ setExpanded(false);
190
+ event.currentTarget.blur();
191
+ }
192
+ }}
193
+ />
194
+ </label>
195
+ </div>
196
+ );
183
197
  }
@@ -1,57 +1,66 @@
1
- import { forwardRef, type InputHTMLAttributes } from "react"
2
- import { SearchIcon } from "../../icons/icons"
3
- import { cn } from "../../lib/class-names"
4
- import { ExpandingSearchField } from "./expanding-search-field"
1
+ import { forwardRef, type InputHTMLAttributes } from "react";
2
+ import { SearchIcon } from "../../icons/icons";
3
+ import { cn } from "../../lib/class-names";
4
+ import { ExpandingSearchField } from "./expanding-search-field";
5
5
 
6
- export type SearchFieldSize = "sm" | "md" | "lg"
6
+ export type SearchFieldSize = "sm" | "md" | "lg";
7
7
 
8
8
  const pillSizes: Record<SearchFieldSize, string> = {
9
- sm: "control-field flex items-center gap-1.5 rounded-full px-2.5 py-1 text-caption text-label-tertiary",
10
- md: "control-field flex items-center gap-2 rounded-full px-3 py-2 text-subheadline text-label-tertiary",
11
- lg: "control-field flex items-center gap-2.5 rounded-full px-4 py-2.5 text-body text-label-tertiary",
12
- }
9
+ sm: "control-field flex items-center gap-1.5 rounded-full px-2.5 py-1 text-caption text-label-tertiary",
10
+ md: "control-field flex items-center gap-2 rounded-full px-3 py-2 text-subheadline text-label-tertiary",
11
+ lg: "control-field flex items-center gap-2.5 rounded-full px-4 py-2.5 text-body text-label-tertiary",
12
+ };
13
13
  const iconSizes: Record<SearchFieldSize, number> = {
14
- sm: 13,
15
- md: 15,
16
- lg: 17,
17
- }
14
+ sm: 13,
15
+ md: 15,
16
+ lg: 17,
17
+ };
18
18
 
19
- export interface SearchFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
20
- /** Expand a single pill from its search icon on focus. @default false */
21
- collapsible?: boolean
22
- /** Pill height, icon size and text scale (non-collapsible variant). @default "md" */
23
- size?: SearchFieldSize
19
+ export interface SearchFieldProps
20
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
21
+ /** Expand a single pill from its search icon on focus. @default false */
22
+ collapsible?: boolean;
23
+ /** Pill height, icon size and text scale (non-collapsible variant). @default "md" */
24
+ size?: SearchFieldSize;
24
25
  }
25
26
 
26
- export const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(
27
- { className, placeholder = "Search", collapsible = false, size = "md", ...props },
28
- ref
29
- ) {
30
- if (collapsible)
31
- return (
32
- <ExpandingSearchField
33
- {...props}
34
- inputRef={ref}
35
- className={className}
36
- placeholder={placeholder}
37
- />
38
- )
39
- const accessibleName = props["aria-labelledby"]
40
- ? undefined
41
- : (props["aria-label"] ?? placeholder)
42
- return (
43
- <label className={cn(pillSizes[size], className)}>
44
- <span aria-hidden="true" className="shrink-0">
45
- <SearchIcon size={iconSizes[size]} strokeWidth={2} />
46
- </span>
47
- <input
48
- ref={ref}
49
- type="search"
50
- placeholder={placeholder}
51
- aria-label={accessibleName}
52
- className="min-w-0 grow bg-transparent text-label outline-none placeholder:text-label-tertiary"
53
- {...props}
54
- />
55
- </label>
56
- )
57
- })
27
+ export const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(
28
+ function SearchField(
29
+ {
30
+ className,
31
+ placeholder = "Search",
32
+ collapsible = false,
33
+ size = "md",
34
+ ...props
35
+ },
36
+ ref,
37
+ ) {
38
+ if (collapsible)
39
+ return (
40
+ <ExpandingSearchField
41
+ {...props}
42
+ inputRef={ref}
43
+ className={className}
44
+ placeholder={placeholder}
45
+ />
46
+ );
47
+ const accessibleName = props["aria-labelledby"]
48
+ ? undefined
49
+ : (props["aria-label"] ?? placeholder);
50
+ return (
51
+ <label className={cn(pillSizes[size], className)}>
52
+ <span aria-hidden="true" className="shrink-0">
53
+ <SearchIcon size={iconSizes[size]} strokeWidth={2} />
54
+ </span>
55
+ <input
56
+ ref={ref}
57
+ type="search"
58
+ placeholder={placeholder}
59
+ aria-label={accessibleName}
60
+ className="text-label placeholder:text-label-tertiary min-w-0 grow bg-transparent outline-none"
61
+ {...props}
62
+ />
63
+ </label>
64
+ );
65
+ },
66
+ );