@seed-design/lynx-react 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/lib/components/Accordion/Accordion.jsx +2 -2
  2. package/lib/components/ActionButton/ActionButton.jsx +1 -1
  3. package/lib/components/AlertDialog/AlertDialog.d.ts +64 -0
  4. package/lib/components/AlertDialog/AlertDialog.d.ts.map +1 -0
  5. package/lib/components/AlertDialog/AlertDialog.jsx +138 -0
  6. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts +2 -0
  7. package/lib/components/AlertDialog/AlertDialog.namespace.d.ts.map +1 -0
  8. package/lib/components/AlertDialog/AlertDialog.namespace.js +17 -0
  9. package/lib/components/AlertDialog/index.d.ts +3 -0
  10. package/lib/components/AlertDialog/index.d.ts.map +1 -0
  11. package/lib/components/AppBar/AppBar.d.ts +14 -1
  12. package/lib/components/AppBar/AppBar.d.ts.map +1 -1
  13. package/lib/components/AppBar/AppBar.jsx +42 -16
  14. package/lib/components/AppBar/AppBar.namespace.d.ts +1 -1
  15. package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -1
  16. package/lib/components/AppBar/context.d.ts +2 -1
  17. package/lib/components/AppBar/context.d.ts.map +1 -1
  18. package/lib/components/AppBar/index.d.ts +1 -1
  19. package/lib/components/AppBar/index.d.ts.map +1 -1
  20. package/lib/components/AppBar/useAppBar.d.ts +2 -1
  21. package/lib/components/AppBar/useAppBar.d.ts.map +1 -1
  22. package/lib/components/AppBar/useAppBar.js +17 -15
  23. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts +182 -0
  24. package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts.map +1 -0
  25. package/lib/components/AttachmentDisplay/AttachmentDisplay.jsx +444 -0
  26. package/lib/components/AttachmentDisplay/index.d.ts +2 -0
  27. package/lib/components/AttachmentDisplay/index.d.ts.map +1 -0
  28. package/lib/components/AttachmentInput/AttachmentInput.d.ts +179 -0
  29. package/lib/components/AttachmentInput/AttachmentInput.d.ts.map +1 -0
  30. package/lib/components/AttachmentInput/AttachmentInput.jsx +520 -0
  31. package/lib/components/AttachmentInput/index.d.ts +2 -0
  32. package/lib/components/AttachmentInput/index.d.ts.map +1 -0
  33. package/lib/components/BottomSheet/BottomSheet.d.ts +1 -1
  34. package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
  35. package/lib/components/BottomSheet/BottomSheet.jsx +1 -7
  36. package/lib/components/Box/Box.d.ts +3 -3
  37. package/lib/components/Box/Box.d.ts.map +1 -1
  38. package/lib/components/Callout/Callout.jsx +2 -2
  39. package/lib/components/Checkbox/Checkbox.jsx +2 -2
  40. package/lib/components/Chip/Chip.jsx +2 -2
  41. package/lib/components/ChipTabs/ChipTabs.d.ts +41 -0
  42. package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -0
  43. package/lib/components/ChipTabs/ChipTabs.jsx +39 -0
  44. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts +2 -0
  45. package/lib/components/ChipTabs/ChipTabs.namespace.d.ts.map +1 -0
  46. package/lib/components/ChipTabs/ChipTabs.namespace.js +13 -0
  47. package/lib/components/ChipTabs/index.d.ts +3 -0
  48. package/lib/components/ChipTabs/index.d.ts.map +1 -0
  49. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts +18 -0
  50. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -0
  51. package/lib/components/ContextualFloatingButton/ContextualFloatingButton.jsx +113 -0
  52. package/lib/components/ContextualFloatingButton/index.d.ts +2 -0
  53. package/lib/components/ContextualFloatingButton/index.d.ts.map +1 -0
  54. package/lib/components/Dialog/Dialog.d.ts +87 -0
  55. package/lib/components/Dialog/Dialog.d.ts.map +1 -0
  56. package/lib/components/Dialog/Dialog.jsx +147 -0
  57. package/lib/components/Dialog/Dialog.namespace.d.ts +2 -0
  58. package/lib/components/Dialog/Dialog.namespace.d.ts.map +1 -0
  59. package/lib/components/Dialog/Dialog.namespace.js +18 -0
  60. package/lib/components/Dialog/index.d.ts +3 -0
  61. package/lib/components/Dialog/index.d.ts.map +1 -0
  62. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts +23 -0
  63. package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -0
  64. package/lib/components/FloatingActionButton/FloatingActionButton.jsx +76 -0
  65. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts +2 -0
  66. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts.map +1 -0
  67. package/lib/components/FloatingActionButton/FloatingActionButton.namespace.js +10 -0
  68. package/lib/components/FloatingActionButton/index.d.ts +3 -0
  69. package/lib/components/FloatingActionButton/index.d.ts.map +1 -0
  70. package/lib/components/HelpBubble/HelpBubble.d.ts +67 -0
  71. package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -0
  72. package/lib/components/HelpBubble/HelpBubble.jsx +600 -0
  73. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts +2 -0
  74. package/lib/components/HelpBubble/HelpBubble.namespace.d.ts.map +1 -0
  75. package/lib/components/HelpBubble/HelpBubble.namespace.js +18 -0
  76. package/lib/components/HelpBubble/index.d.ts +3 -0
  77. package/lib/components/HelpBubble/index.d.ts.map +1 -0
  78. package/lib/components/Icon/Icon.d.ts +1 -0
  79. package/lib/components/Icon/Icon.d.ts.map +1 -1
  80. package/lib/components/Icon/Icon.jsx +2 -1
  81. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts +28 -0
  82. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts.map +1 -0
  83. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.jsx +37 -0
  84. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts +2 -0
  85. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts.map +1 -0
  86. package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.js +9 -0
  87. package/lib/components/IdentityPlaceholder/identity-placeholder-business.js +4 -0
  88. package/lib/components/IdentityPlaceholder/identity-placeholder-person.js +4 -0
  89. package/lib/components/IdentityPlaceholder/index.d.ts +3 -0
  90. package/lib/components/IdentityPlaceholder/index.d.ts.map +1 -0
  91. package/lib/components/InputButton/InputButton.d.ts +2 -0
  92. package/lib/components/InputButton/InputButton.d.ts.map +1 -1
  93. package/lib/components/InputButton/InputButton.jsx +40 -8
  94. package/lib/components/List/List.d.ts +1 -1
  95. package/lib/components/List/List.d.ts.map +1 -1
  96. package/lib/components/List/List.jsx +26 -12
  97. package/lib/components/Menu/Menu.d.ts.map +1 -1
  98. package/lib/components/Menu/Menu.jsx +15 -5
  99. package/lib/components/PageBanner/PageBanner.d.ts +3 -0
  100. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
  101. package/lib/components/PageBanner/PageBanner.jsx +49 -11
  102. package/lib/components/QuantityPicker/QuantityPicker.d.ts +53 -0
  103. package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
  104. package/lib/components/QuantityPicker/QuantityPicker.jsx +282 -0
  105. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
  106. package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
  107. package/lib/components/QuantityPicker/QuantityPicker.namespace.js +11 -0
  108. package/lib/components/QuantityPicker/index.d.ts +3 -0
  109. package/lib/components/QuantityPicker/index.d.ts.map +1 -0
  110. package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
  111. package/lib/components/ReactionButton/ReactionButton.d.ts +1 -1
  112. package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
  113. package/lib/components/ReactionButton/ReactionButton.jsx +22 -13
  114. package/lib/components/ScrollFog/ScrollFog.d.ts +41 -0
  115. package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -0
  116. package/lib/components/ScrollFog/ScrollFog.jsx +65 -0
  117. package/lib/components/ScrollFog/index.d.ts +2 -0
  118. package/lib/components/ScrollFog/index.d.ts.map +1 -0
  119. package/lib/components/SegmentedControl/SegmentedControl.jsx +2 -2
  120. package/lib/components/Select/Select.d.ts +112 -0
  121. package/lib/components/Select/Select.d.ts.map +1 -0
  122. package/lib/components/Select/Select.jsx +885 -0
  123. package/lib/components/Select/Select.namespace.d.ts +2 -0
  124. package/lib/components/Select/Select.namespace.d.ts.map +1 -0
  125. package/lib/components/Select/Select.namespace.js +23 -0
  126. package/lib/components/Select/index.d.ts +3 -0
  127. package/lib/components/Select/index.d.ts.map +1 -0
  128. package/lib/components/Skeleton/Skeleton.d.ts +15 -0
  129. package/lib/components/Skeleton/Skeleton.d.ts.map +1 -0
  130. package/lib/components/Skeleton/Skeleton.jsx +18 -0
  131. package/lib/components/Skeleton/index.d.ts +2 -0
  132. package/lib/components/Skeleton/index.d.ts.map +1 -0
  133. package/lib/components/Slider/Slider.d.ts +101 -0
  134. package/lib/components/Slider/Slider.d.ts.map +1 -0
  135. package/lib/components/Slider/Slider.jsx +573 -0
  136. package/lib/components/Slider/Slider.namespace.d.ts +2 -0
  137. package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
  138. package/lib/components/Slider/Slider.namespace.js +19 -0
  139. package/lib/components/Slider/Slider.utils.d.ts +19 -0
  140. package/lib/components/Slider/Slider.utils.d.ts.map +1 -0
  141. package/lib/components/Slider/Slider.utils.js +106 -0
  142. package/lib/components/Slider/index.d.ts +3 -0
  143. package/lib/components/Slider/index.d.ts.map +1 -0
  144. package/lib/components/Stack/Stack.d.ts +7 -9
  145. package/lib/components/Stack/Stack.d.ts.map +1 -1
  146. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
  147. package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +22 -8
  148. package/lib/components/Switch/Switch.d.ts.map +1 -1
  149. package/lib/components/Switch/Switch.jsx +5 -3
  150. package/lib/components/Tabs/Tabs.d.ts +34 -2
  151. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  152. package/lib/components/Tabs/Tabs.jsx +186 -56
  153. package/lib/components/Tabs/Tabs.utils.d.ts +14 -1
  154. package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
  155. package/lib/components/Tabs/Tabs.utils.js +24 -3
  156. package/lib/components/Tabs/index.d.ts +1 -1
  157. package/lib/components/Tabs/index.d.ts.map +1 -1
  158. package/lib/components/index.d.ts +14 -0
  159. package/lib/components/index.d.ts.map +1 -1
  160. package/lib/hooks/useControllableState.d.ts +1 -14
  161. package/lib/hooks/useControllableState.d.ts.map +1 -1
  162. package/lib/hooks/usePressTap.d.ts +1 -27
  163. package/lib/hooks/usePressTap.d.ts.map +1 -1
  164. package/lib/hooks/useSafeArea.d.ts +8 -4
  165. package/lib/hooks/useSafeArea.d.ts.map +1 -1
  166. package/lib/hooks/useSafeArea.js +22 -20
  167. package/lib/index.js +29 -6
  168. package/lib/{hooks/useControllableState.js → packages/lynx-react-headless/use-controllable-state/lib/index.js} +2 -3
  169. package/lib/{hooks/usePressTap.js → packages/lynx-react-headless/use-press-tap/lib/index.js} +13 -10
  170. package/lib/types.d.ts +1 -0
  171. package/lib/types.d.ts.map +1 -1
  172. package/lib/utils/styled.d.ts +46 -6
  173. package/lib/utils/styled.d.ts.map +1 -1
  174. package/lib/utils/styled.js +19 -4
  175. package/package.json +9 -5
  176. package/src/components/AlertDialog/AlertDialog.namespace.ts +22 -0
  177. package/src/components/AlertDialog/AlertDialog.test.tsx +226 -0
  178. package/src/components/AlertDialog/AlertDialog.tsx +374 -0
  179. package/src/components/AlertDialog/index.ts +24 -0
  180. package/src/components/AppBar/AppBar.namespace.ts +1 -0
  181. package/src/components/AppBar/AppBar.test.tsx +252 -3
  182. package/src/components/AppBar/AppBar.tsx +108 -25
  183. package/src/components/AppBar/context.ts +3 -1
  184. package/src/components/AppBar/index.ts +1 -0
  185. package/src/components/AppBar/useAppBar.ts +21 -15
  186. package/src/components/AttachmentDisplay/AttachmentDisplay.tsx +886 -0
  187. package/src/components/AttachmentDisplay/index.ts +48 -0
  188. package/src/components/AttachmentInput/AttachmentInput.tsx +1001 -0
  189. package/src/components/AttachmentInput/index.ts +49 -0
  190. package/src/components/BottomSheet/BottomSheet.test.tsx +22 -7
  191. package/src/components/BottomSheet/BottomSheet.tsx +1 -19
  192. package/src/components/Box/Box.test.tsx +104 -0
  193. package/src/components/Box/Box.tsx +9 -6
  194. package/src/components/ChipTabs/ChipTabs.namespace.ts +14 -0
  195. package/src/components/ChipTabs/ChipTabs.tsx +119 -0
  196. package/src/components/ChipTabs/index.ts +16 -0
  197. package/src/components/ContextualFloatingButton/ContextualFloatingButton.test.tsx +63 -0
  198. package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +232 -0
  199. package/src/components/ContextualFloatingButton/index.ts +4 -0
  200. package/src/components/Dialog/Dialog.namespace.ts +24 -0
  201. package/src/components/Dialog/Dialog.test.tsx +172 -0
  202. package/src/components/Dialog/Dialog.tsx +373 -0
  203. package/src/components/Dialog/index.ts +26 -0
  204. package/src/components/FloatingActionButton/FloatingActionButton.namespace.ts +8 -0
  205. package/src/components/FloatingActionButton/FloatingActionButton.test.tsx +59 -0
  206. package/src/components/FloatingActionButton/FloatingActionButton.tsx +207 -0
  207. package/src/components/FloatingActionButton/index.ts +10 -0
  208. package/src/components/HelpBubble/HelpBubble.namespace.ts +24 -0
  209. package/src/components/HelpBubble/HelpBubble.tsx +998 -0
  210. package/src/components/HelpBubble/index.ts +26 -0
  211. package/src/components/Icon/Icon.test.tsx +0 -3
  212. package/src/components/Icon/Icon.tsx +11 -1
  213. package/src/components/IdentityPlaceholder/IdentityPlaceholder.namespace.ts +6 -0
  214. package/src/components/IdentityPlaceholder/IdentityPlaceholder.tsx +103 -0
  215. package/src/components/IdentityPlaceholder/assets.d.ts +4 -0
  216. package/src/components/IdentityPlaceholder/identity-placeholder-business.webp +0 -0
  217. package/src/components/IdentityPlaceholder/identity-placeholder-person.webp +0 -0
  218. package/src/components/IdentityPlaceholder/index.ts +8 -0
  219. package/src/components/InputButton/InputButton.test.tsx +128 -1
  220. package/src/components/InputButton/InputButton.tsx +60 -9
  221. package/src/components/List/List.test.tsx +54 -2
  222. package/src/components/List/List.tsx +43 -10
  223. package/src/components/Menu/Menu.tsx +14 -4
  224. package/src/components/Menu/__tests__/Menu.test.tsx +34 -0
  225. package/src/components/PageBanner/PageBanner.test.tsx +110 -3
  226. package/src/components/PageBanner/PageBanner.tsx +83 -19
  227. package/src/components/QuantityPicker/QuantityPicker.namespace.ts +10 -0
  228. package/src/components/QuantityPicker/QuantityPicker.test.tsx +235 -0
  229. package/src/components/QuantityPicker/QuantityPicker.tsx +605 -0
  230. package/src/components/QuantityPicker/index.ts +14 -0
  231. package/src/components/ReactionButton/ReactionButton.feedback.test.tsx +50 -0
  232. package/src/components/ReactionButton/ReactionButton.test.tsx +5 -3
  233. package/src/components/ReactionButton/ReactionButton.tsx +20 -12
  234. package/src/components/ScrollFog/ScrollFog.test.tsx +222 -0
  235. package/src/components/ScrollFog/ScrollFog.tsx +172 -0
  236. package/src/components/ScrollFog/index.ts +1 -0
  237. package/src/components/Select/Select.namespace.ts +34 -0
  238. package/src/components/Select/Select.test.tsx +297 -0
  239. package/src/components/Select/Select.tsx +1483 -0
  240. package/src/components/Select/index.ts +39 -0
  241. package/src/components/Skeleton/Skeleton.test.tsx +64 -0
  242. package/src/components/Skeleton/Skeleton.tsx +45 -0
  243. package/src/components/Skeleton/index.ts +1 -0
  244. package/src/components/Slider/Slider.namespace.ts +26 -0
  245. package/src/components/Slider/Slider.test.tsx +260 -0
  246. package/src/components/Slider/Slider.tsx +897 -0
  247. package/src/components/Slider/Slider.utils.ts +167 -0
  248. package/src/components/Slider/index.ts +30 -0
  249. package/src/components/Stack/Stack.test.tsx +7 -0
  250. package/src/components/Stack/Stack.tsx +11 -10
  251. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +21 -1
  252. package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +30 -7
  253. package/src/components/Switch/Switch.tsx +14 -13
  254. package/src/components/Tabs/Tabs.test.tsx +218 -1
  255. package/src/components/Tabs/Tabs.tsx +346 -75
  256. package/src/components/Tabs/Tabs.utils.ts +51 -1
  257. package/src/components/Tabs/index.ts +17 -1
  258. package/src/components/index.ts +14 -0
  259. package/src/hooks/useControllableState.ts +1 -43
  260. package/src/hooks/usePressTap.test.ts +23 -11
  261. package/src/hooks/usePressTap.ts +1 -60
  262. package/src/hooks/useSafeArea.test.ts +13 -1
  263. package/src/hooks/useSafeArea.ts +25 -23
  264. package/src/types.ts +1 -0
  265. package/src/utils/styled.ts +127 -9
@@ -2,6 +2,7 @@ import "@testing-library/jest-dom";
2
2
  import * as React from "@lynx-js/react";
3
3
  import type { MainThread } from "@lynx-js/types";
4
4
  import type { LynxIconElementProps } from "../../types";
5
+ import { Fragment, useEffect } from "@lynx-js/react";
5
6
  import {
6
7
  act,
7
8
  fireEvent,
@@ -21,7 +22,9 @@ interface TestLynxGlobal {
21
22
  lynx?: {
22
23
  __globalProps?: {
23
24
  safeAreaInsetTop?: number;
25
+ safeAreaInsetRight?: number;
24
26
  safeAreaInsetBottom?: number;
27
+ safeAreaInsetLeft?: number;
25
28
  };
26
29
  };
27
30
  lynxTestingEnv?: {
@@ -152,11 +155,22 @@ describe("AppBar", () => {
152
155
  render(<AppBar.Root theme="cupertino" />);
153
156
 
154
157
  expectStyle(getAppBarRoot().style, {
155
- height: "calc(91px)",
158
+ height: "calc(103px)",
156
159
  "padding-top": "47px",
157
160
  });
158
161
  });
159
162
 
163
+ it("adds left and right safe area insets to the root padding", () => {
164
+ setGlobalProps({ safeAreaInsetRight: 0, safeAreaInsetLeft: 59 });
165
+
166
+ render(<AppBar.Root theme="cupertino" />);
167
+
168
+ expectStyle(getAppBarRoot().style, {
169
+ "padding-left": "calc(75px)",
170
+ "padding-right": "calc(16px)",
171
+ });
172
+ });
173
+
160
174
  it("does not read the safe area CSS variable override as the JS layout source", () => {
161
175
  setGlobalProps({ safeAreaInsetTop: 47 });
162
176
 
@@ -168,7 +182,7 @@ describe("AppBar", () => {
168
182
  );
169
183
 
170
184
  expectStyle(getAppBarRoot().style, {
171
- height: "calc(91px)",
185
+ height: "calc(103px)",
172
186
  "padding-top": "47px",
173
187
  });
174
188
  });
@@ -239,6 +253,8 @@ describe("AppBar", () => {
239
253
  });
240
254
 
241
255
  it("updates centered title padding from left and right layout widths", async () => {
256
+ setGlobalProps({ safeAreaInsetRight: 0, safeAreaInsetLeft: 59 });
257
+
242
258
  render(
243
259
  <AppBar.Root>
244
260
  <AppBar.Left>
@@ -268,9 +284,242 @@ describe("AppBar", () => {
268
284
  });
269
285
 
270
286
  await waitFor(() => {
271
- expect(main).toHaveStyle({ paddingLeft: "72px", paddingRight: "72px" });
287
+ expect(main).toHaveStyle({
288
+ paddingLeft: "calc(59px + 88px)",
289
+ paddingRight: "calc(0px + 88px)",
290
+ });
272
291
  });
273
292
  });
293
+
294
+ it.each([
295
+ "left",
296
+ "right",
297
+ ] as const)("bleeds both sides of a single button in the %s slot", (side) => {
298
+ const Slot = side === "left" ? AppBar.Left : AppBar.Right;
299
+ render(
300
+ <AppBar.Root>
301
+ <Slot>
302
+ <AppBar.IconButton accessibility-label="Back" />
303
+ </Slot>
304
+ </AppBar.Root>,
305
+ );
306
+ const button = getAppBarRoot().querySelector(".seed-app-bar__iconButton");
307
+ expect(button).toHaveClass("seed-app-bar__icon-button-edge-leading");
308
+ expect(button).toHaveClass("seed-app-bar__icon-button-edge-trailing");
309
+ });
310
+
311
+ it.each([
312
+ "left",
313
+ "right",
314
+ ] as const)("bleeds only the outer sides of buttons in the %s slot", (side) => {
315
+ const Slot = side === "left" ? AppBar.Left : AppBar.Right;
316
+ render(
317
+ <AppBar.Root>
318
+ <Slot>
319
+ <AppBar.IconButton accessibility-label="Search" />
320
+ <AppBar.IconButton accessibility-label="Share" />
321
+ <AppBar.IconButton accessibility-label="Close" />
322
+ </Slot>
323
+ </AppBar.Root>,
324
+ );
325
+ const buttons = getAppBarRoot().querySelectorAll(".seed-app-bar__iconButton");
326
+ expect(buttons).toHaveLength(3);
327
+ expect(buttons[0]).toHaveClass("seed-app-bar__icon-button-edge-leading");
328
+ expect(buttons[0]).not.toHaveClass("seed-app-bar__icon-button-edge-trailing");
329
+ expect(buttons[1]).not.toHaveClass("seed-app-bar__icon-button-edge-leading");
330
+ expect(buttons[1]).not.toHaveClass("seed-app-bar__icon-button-edge-trailing");
331
+ expect(buttons[2]).not.toHaveClass("seed-app-bar__icon-button-edge-leading");
332
+ expect(buttons[2]).toHaveClass("seed-app-bar__icon-button-edge-trailing");
333
+ });
334
+
335
+ it("updates both edges when a conditional sibling changes without remounting the button", () => {
336
+ const mount = vi.fn();
337
+ function WrappedButton() {
338
+ useEffect(() => {
339
+ mount();
340
+ }, []);
341
+ return <AppBar.IconButton accessibility-label="Back" />;
342
+ }
343
+ function Example({ showCustom }: { showCustom: boolean }) {
344
+ return (
345
+ <AppBar.Root>
346
+ <AppBar.Left>
347
+ <WrappedButton />
348
+ {showCustom && (
349
+ <AppBar.Slot>
350
+ <text>Done</text>
351
+ </AppBar.Slot>
352
+ )}
353
+ </AppBar.Left>
354
+ </AppBar.Root>
355
+ );
356
+ }
357
+ const { rerender } = render(<Example showCustom={false} />);
358
+ const button = () => getAppBarRoot().querySelector(".seed-app-bar__iconButton");
359
+ expect(button()).toHaveClass("seed-app-bar__icon-button-edge-leading");
360
+ expect(button()).toHaveClass("seed-app-bar__icon-button-edge-trailing");
361
+ rerender(<Example showCustom />);
362
+ expect(button()).toHaveClass("seed-app-bar__icon-button-edge-leading");
363
+ expect(button()).not.toHaveClass("seed-app-bar__icon-button-edge-trailing");
364
+ rerender(<Example showCustom={false} />);
365
+ expect(button()).toHaveClass("seed-app-bar__icon-button-edge-trailing");
366
+ expect(mount).toHaveBeenCalledTimes(1);
367
+ });
368
+
369
+ it("does not bleed a custom slot sitting at the slot edge", () => {
370
+ render(
371
+ <AppBar.Root>
372
+ <AppBar.Right>
373
+ <AppBar.Slot>
374
+ <text>Done</text>
375
+ </AppBar.Slot>
376
+ </AppBar.Right>
377
+ </AppBar.Root>,
378
+ );
379
+
380
+ const custom = getAppBarRoot().querySelector<HTMLElement>(".seed-app-bar__custom");
381
+ expect(custom).toBeInTheDocument();
382
+ expect(custom!.style.getPropertyValue("margin-left")).toBe("");
383
+ expect(custom!.style.getPropertyValue("margin-right")).toBe("");
384
+ });
385
+
386
+ it("bleeds the trailing icon button but not a preceding custom slot", () => {
387
+ render(
388
+ <AppBar.Root>
389
+ <AppBar.Right>
390
+ <AppBar.Slot>
391
+ <text>Done</text>
392
+ </AppBar.Slot>
393
+ <AppBar.IconButton accessibility-label="Close" />
394
+ </AppBar.Right>
395
+ </AppBar.Root>,
396
+ );
397
+
398
+ const root = getAppBarRoot();
399
+ const custom = root.querySelector<HTMLElement>(".seed-app-bar__custom");
400
+ const iconButton = root.querySelector<HTMLElement>(".seed-app-bar__iconButton");
401
+ expect(custom!.style.getPropertyValue("margin-right")).toBe("");
402
+ expect(iconButton).toHaveClass("seed-app-bar__icon-button-edge-trailing");
403
+ });
404
+
405
+ it.each([
406
+ "leading",
407
+ "trailing",
408
+ "both",
409
+ ] as const)("respects explicit edge=%s over automatic edges", (edge) => {
410
+ render(
411
+ <AppBar.Root>
412
+ <AppBar.Left>
413
+ <AppBar.IconButton accessibility-label="Back" edge={edge} />
414
+ </AppBar.Left>
415
+ </AppBar.Root>,
416
+ );
417
+ const button = getAppBarRoot().querySelector(".seed-app-bar__iconButton");
418
+ expect(button?.classList.contains("seed-app-bar__icon-button-edge-leading")).toBe(
419
+ edge !== "trailing",
420
+ );
421
+ expect(button?.classList.contains("seed-app-bar__icon-button-edge-trailing")).toBe(
422
+ edge !== "leading",
423
+ );
424
+ });
425
+
426
+ it("preserves caller styles when automatic edge compensation applies", () => {
427
+ render(
428
+ <AppBar.Root>
429
+ <AppBar.Left>
430
+ <AppBar.IconButton
431
+ accessibility-label="Back"
432
+ {...{ style: { width: "60px", opacity: 0.5 } }}
433
+ />
434
+ </AppBar.Left>
435
+ </AppBar.Root>,
436
+ );
437
+ const button = getAppBarRoot().querySelector<HTMLElement>(".seed-app-bar__iconButton");
438
+ expect(button).toHaveClass("seed-app-bar__icon-button-edge-leading");
439
+ expectStyle(button!.style, { width: "60px", opacity: "0.5" });
440
+ });
441
+
442
+ it("finds the physical edge through nested fragments and conditional children", () => {
443
+ render(
444
+ <AppBar.Root>
445
+ <AppBar.Right>
446
+ <Fragment>
447
+ {false && <AppBar.IconButton accessibility-label="Hidden" />}
448
+ <AppBar.IconButton accessibility-label="Search" />
449
+ <Fragment key="actions">
450
+ <AppBar.IconButton accessibility-label="Close" />
451
+ {null}
452
+ <Fragment />
453
+ </Fragment>
454
+ </Fragment>
455
+ </AppBar.Right>
456
+ </AppBar.Root>,
457
+ );
458
+ const buttons = getAppBarRoot().querySelectorAll<HTMLElement>(".seed-app-bar__iconButton");
459
+ expect(buttons).toHaveLength(2);
460
+ expect(buttons[0]).toHaveClass("seed-app-bar__icon-button-edge-leading");
461
+ expect(buttons[0]).not.toHaveClass("seed-app-bar__icon-button-edge-trailing");
462
+ expect(buttons[1]).not.toHaveClass("seed-app-bar__icon-button-edge-leading");
463
+ expect(buttons[1]).toHaveClass("seed-app-bar__icon-button-edge-trailing");
464
+ });
465
+
466
+ it("does not leak edge to native children or bleed buttons inside custom layout boxes", () => {
467
+ render(
468
+ <AppBar.Root>
469
+ <AppBar.Left>
470
+ <view id="native-edge">
471
+ <AppBar.IconButton accessibility-label="Nested" />
472
+ </view>
473
+ </AppBar.Left>
474
+ <AppBar.Right>
475
+ <AppBar.Slot>
476
+ <AppBar.IconButton accessibility-label="Custom" />
477
+ </AppBar.Slot>
478
+ </AppBar.Right>
479
+ </AppBar.Root>,
480
+ );
481
+ const root = getAppBarRoot();
482
+ expect(root.querySelector("#native-edge")).not.toHaveAttribute("edge");
483
+ expect(root.querySelector("[edge]")).toBeNull();
484
+ for (const button of root.querySelectorAll(".seed-app-bar__iconButton")) {
485
+ expect(button).not.toHaveClass("seed-app-bar__icon-button-edge-leading");
486
+ expect(button).not.toHaveClass("seed-app-bar__icon-button-edge-trailing");
487
+ }
488
+ });
489
+
490
+ it("passes edge through wrapper components without changing their props or remounting", () => {
491
+ const mount = vi.fn();
492
+ const received = vi.fn();
493
+ function WrappedButton(props: { edge?: "leading" | "trailing" }) {
494
+ received(props);
495
+ useEffect(() => {
496
+ mount();
497
+ }, []);
498
+ return <AppBar.IconButton accessibility-label="Wrapped" {...props} />;
499
+ }
500
+ const { rerender } = render(
501
+ <AppBar.Root>
502
+ <AppBar.Left>
503
+ <WrappedButton />
504
+ </AppBar.Left>
505
+ </AppBar.Root>,
506
+ );
507
+ expect(getAppBarRoot().querySelector(".seed-app-bar__iconButton")).toHaveClass(
508
+ "seed-app-bar__icon-button-edge-leading",
509
+ );
510
+ expect(received.mock.lastCall?.[0]).not.toHaveProperty("edge");
511
+ rerender(
512
+ <AppBar.Root>
513
+ <AppBar.Left>
514
+ <WrappedButton edge="trailing" />
515
+ </AppBar.Left>
516
+ </AppBar.Root>,
517
+ );
518
+ expect(getAppBarRoot().querySelector(".seed-app-bar__iconButton")).toHaveClass(
519
+ "seed-app-bar__icon-button-edge-trailing",
520
+ );
521
+ expect(mount).toHaveBeenCalledTimes(1);
522
+ });
274
523
  });
275
524
 
276
525
  describe("AppBar icon recipe ownership", () => {
@@ -21,6 +21,7 @@ import { IconSlotProvider } from "../Icon/Icon";
21
21
  import { AppBarProvider, useAppBarContext } from "./context";
22
22
  import { getLayoutWidth, getMainLayoutStyle, useAppBar } from "./useAppBar";
23
23
  import { mergeProps } from "../../utils/merge-props";
24
+ import { useScaleFeedback } from "../../hooks/useScaleFeedback";
24
25
 
25
26
  type AppBarClassNames = ReturnType<typeof appBar>;
26
27
  type AppBarMainClassNames = ReturnType<typeof appBarMain>;
@@ -49,6 +50,66 @@ function useAppBarMainClassNames(consumer: string): AppBarMainClassNames {
49
50
  }
50
51
  }
51
52
 
53
+ /**
54
+ * 아이콘 버튼의 bleed 보정 방향. `leading`은 왼쪽, `trailing`은 오른쪽, `both`는 양쪽.
55
+ */
56
+ export type AppBarEdge = "leading" | "trailing" | "both";
57
+
58
+ const AppBarEdgeContext = React.createContext<AppBarEdge | undefined>(undefined);
59
+
60
+ /** Fragments and arrays do not create layout boxes; retain their keys and nesting. */
61
+ function countEdgeChildren(children: React.ReactNode): number {
62
+ if (children == null || children === "" || typeof children === "boolean") return 0;
63
+ if (Array.isArray(children))
64
+ return children.reduce((count, child) => count + countEdgeChildren(child), 0);
65
+ if (
66
+ React.isValidElement<{ children?: React.ReactNode }>(children) &&
67
+ children.type === React.Fragment
68
+ ) {
69
+ return countEdgeChildren(children.props.children);
70
+ }
71
+ return 1;
72
+ }
73
+
74
+ // Lynx lacks :first-child/:last-child; pass both slot edges through Context to match
75
+ // web spacing without shrinking the icon button's touch target.
76
+ function provideEdgeToChildren(children: React.ReactNode): React.ReactNode {
77
+ const lastIndex = countEdgeChildren(children) - 1;
78
+ let index = 0;
79
+
80
+ function visit(child: React.ReactNode): React.ReactNode {
81
+ if (child == null || child === "" || typeof child === "boolean") return child;
82
+ if (Array.isArray(child)) return child.map(visit);
83
+ if (
84
+ React.isValidElement<{ children?: React.ReactNode }>(child) &&
85
+ child.type === React.Fragment
86
+ ) {
87
+ return React.cloneElement(child, { children: visit(child.props.children) });
88
+ }
89
+
90
+ const childIndex = index++;
91
+ const edge =
92
+ childIndex === 0
93
+ ? childIndex === lastIndex
94
+ ? "both"
95
+ : "leading"
96
+ : childIndex === lastIndex
97
+ ? "trailing"
98
+ : undefined;
99
+ if (!React.isValidElement(child)) return child;
100
+ // Native boxes and custom slots own their layout; do not pass an automatic edge into them.
101
+ const automaticEdge =
102
+ typeof child.type !== "string" && child.type !== AppBarSlot ? edge : undefined;
103
+ return (
104
+ <AppBarEdgeContext.Provider key={child.key} value={automaticEdge}>
105
+ {child}
106
+ </AppBarEdgeContext.Provider>
107
+ );
108
+ }
109
+
110
+ return visit(children);
111
+ }
112
+
52
113
  ////////////////////////////////////////////////////////////////////////////////////
53
114
 
54
115
  export interface AppBarRootProps extends AppBarVariantProps, LynxStyledElementProps {}
@@ -67,7 +128,7 @@ export const AppBarRoot = React.forwardRef<unknown, AppBarRootProps>((props, ref
67
128
  className={clsx(classNames.root, className)}
68
129
  style={
69
130
  {
70
- "--seed-safe-area-top": contextValue.safeAreaInsetTop,
131
+ "--seed-safe-area-top": contextValue.safeArea.safeAreaInsetTop,
71
132
  "--centered-title-padding-x": contextValue.centeredTitlePaddingX,
72
133
  ...rootLayoutStyle,
73
134
  ...style,
@@ -114,7 +175,7 @@ export const AppBarLeft = React.forwardRef<unknown, AppBarLeftProps>((props, ref
114
175
  )}
115
176
  className={clsx(classNames.left, className)}
116
177
  >
117
- {children}
178
+ {provideEdgeToChildren(children)}
118
179
  </view>
119
180
  );
120
181
  });
@@ -149,7 +210,7 @@ export const AppBarRight = React.forwardRef<unknown, AppBarRightProps>((props, r
149
210
  )}
150
211
  className={clsx(classNames.right, className)}
151
212
  >
152
- {children}
213
+ {provideEdgeToChildren(children)}
153
214
  </view>
154
215
  );
155
216
  });
@@ -160,8 +221,7 @@ AppBarRight.displayName = "AppBarRight";
160
221
  export interface AppBarMainProps extends AppBarMainVariantProps, LynxStyledElementProps {}
161
222
 
162
223
  export const AppBarMain = React.forwardRef<unknown, AppBarMainProps>((props, ref) => {
163
- const { centeredTitlePaddingX, safeAreaInsetTop, sharedVariantProps } =
164
- useAppBarContext("AppBarMain");
224
+ const { centeredTitlePaddingX, safeArea, sharedVariantProps } = useAppBarContext("AppBarMain");
165
225
  const [variantProps, otherProps] = appBarMain.splitVariantProps({
166
226
  ...sharedVariantProps,
167
227
  ...props,
@@ -169,13 +229,6 @@ export const AppBarMain = React.forwardRef<unknown, AppBarMainProps>((props, ref
169
229
  const resolvedTheme = variantProps.theme ?? "cupertino";
170
230
  const classNames = appBarMain(variantProps);
171
231
  const { children, className, style, ...nativeProps } = otherProps;
172
- const centeredTitleStyle =
173
- variantProps.theme === "cupertino"
174
- ? {
175
- paddingLeft: centeredTitlePaddingX,
176
- paddingRight: centeredTitlePaddingX,
177
- }
178
- : undefined;
179
232
 
180
233
  return (
181
234
  <AppBarMainClassNamesProvider value={classNames}>
@@ -184,8 +237,7 @@ export const AppBarMain = React.forwardRef<unknown, AppBarMainProps>((props, ref
184
237
  className={clsx(classNames.root, className)}
185
238
  style={
186
239
  {
187
- ...centeredTitleStyle,
188
- ...getMainLayoutStyle(resolvedTheme, safeAreaInsetTop),
240
+ ...getMainLayoutStyle(resolvedTheme, safeArea, centeredTitlePaddingX),
189
241
  ...style,
190
242
  } as LynxViewProps["style"]
191
243
  }
@@ -233,11 +285,18 @@ AppBarSubtitle.displayName = "AppBarSubtitle";
233
285
 
234
286
  ////////////////////////////////////////////////////////////////////////////////////
235
287
 
236
- export interface AppBarIconButtonProps extends LynxElementProps, LynxPressableProps {
288
+ export interface AppBarIconButtonProps
289
+ extends Omit<LynxElementProps, "flatten">,
290
+ LynxPressableProps {
237
291
  icon?: React.ReactElement<LynxIconElementProps>;
238
292
  "accessibility-label"?: LynxViewProps["accessibility-label"];
239
293
  "accessibility-element"?: LynxViewProps["accessibility-element"];
240
294
  "accessibility-traits"?: LynxViewProps["accessibility-traits"];
295
+ /**
296
+ * 슬롯의 첫·마지막 자식이 아이콘 버튼이면 해당 방향을, 유일한 자식이면 양쪽을 자동 보정한다.
297
+ * 명시하면 `AppBarLeft` / `AppBarRight`에서 감지한 자동 보정 방향을 덮어쓴다.
298
+ */
299
+ edge?: AppBarEdge;
241
300
  }
242
301
 
243
302
  export const AppBarIconButton = React.forwardRef<unknown, AppBarIconButtonProps>((props, ref) => {
@@ -245,12 +304,16 @@ export const AppBarIconButton = React.forwardRef<unknown, AppBarIconButtonProps>
245
304
  children,
246
305
  className,
247
306
  icon,
307
+ edge,
248
308
  "accessibility-element": accessibilityElement = true,
249
309
  "accessibility-label": accessibilityLabel,
250
310
  "accessibility-traits": accessibilityTraits = "button",
251
311
  ...nativeProps
252
312
  } = props;
253
313
  const classNames = useAppBarClassNames("AppBarIconButton");
314
+ const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback();
315
+ const automaticEdge = React.useContext(AppBarEdgeContext);
316
+ const resolvedEdge = edge ?? automaticEdge;
254
317
 
255
318
  if (process.env.NODE_ENV !== "production" && accessibilityElement && !accessibilityLabel) {
256
319
  console.warn("AppBarIconButton requires `accessibility-label` for accessibility.");
@@ -258,11 +321,24 @@ export const AppBarIconButton = React.forwardRef<unknown, AppBarIconButtonProps>
258
321
 
259
322
  return (
260
323
  <view
261
- {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
324
+ {...mergeProps(
325
+ ref ? { ref: ref as LynxViewRef } : {},
326
+ scaleFeedbackTriggerProps,
327
+ scaleFeedbackTargetProps,
328
+ nativeProps,
329
+ )}
330
+ flatten={false}
262
331
  accessibility-element={accessibilityElement}
263
332
  accessibility-label={accessibilityLabel}
264
333
  accessibility-traits={accessibilityTraits}
265
- className={clsx(classNames.iconButton, className)}
334
+ className={clsx(
335
+ classNames.iconButton,
336
+ (resolvedEdge === "leading" || resolvedEdge === "both") &&
337
+ "seed-app-bar__icon-button-edge-leading",
338
+ (resolvedEdge === "trailing" || resolvedEdge === "both") &&
339
+ "seed-app-bar__icon-button-edge-trailing",
340
+ className,
341
+ )}
266
342
  >
267
343
  <IconSlotProvider value={{ classNames: { icon: classNames.icon }, deps: [classNames.icon] }}>
268
344
  {icon ? <Icon icon={icon} /> : children}
@@ -272,19 +348,26 @@ export const AppBarIconButton = React.forwardRef<unknown, AppBarIconButtonProps>
272
348
  });
273
349
  AppBarIconButton.displayName = "AppBarIconButton";
274
350
 
275
- export interface AppBarSlotProps extends LynxStyledElementProps {}
351
+ export interface AppBarSlotProps extends LynxStyledElementProps {
352
+ /**
353
+ * @internal 커스텀 슬롯에서는 bleed 보정을 적용하지 않는다. 전달된 값은 native 속성에서 제외한다.
354
+ */
355
+ edge?: AppBarEdge;
356
+ }
276
357
 
277
358
  export const AppBarSlot = React.forwardRef<unknown, AppBarSlotProps>((props, ref) => {
278
- const { children, className, ...nativeProps } = props;
359
+ const { children, className, edge: _edge, ...nativeProps } = props;
279
360
  const classNames = useAppBarClassNames("AppBarSlot");
280
361
 
281
362
  return (
282
- <view
283
- {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
284
- className={clsx(classNames.custom, className)}
285
- >
286
- {children}
287
- </view>
363
+ <AppBarEdgeContext.Provider value={undefined}>
364
+ <view
365
+ {...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
366
+ className={clsx(classNames.custom, className)}
367
+ >
368
+ {children}
369
+ </view>
370
+ </AppBarEdgeContext.Provider>
288
371
  );
289
372
  });
290
373
  AppBarSlot.displayName = "AppBarSlot";
@@ -1,6 +1,8 @@
1
1
  import { type AppBarMainVariantProps } from "@seed-design/lynx-css/recipes/app-bar-main";
2
2
  import * as React from "@lynx-js/react";
3
3
 
4
+ import type { UseSafeAreaReturn } from "../../hooks/useSafeArea";
5
+
4
6
  export type SharedAppBarVariantProps = Pick<
5
7
  AppBarMainVariantProps,
6
8
  "theme" | "tone" | "transitionStyle"
@@ -8,7 +10,7 @@ export type SharedAppBarVariantProps = Pick<
8
10
 
9
11
  export interface AppBarContextValue {
10
12
  centeredTitlePaddingX: string;
11
- safeAreaInsetTop: string;
13
+ safeArea: UseSafeAreaReturn;
12
14
  sharedVariantProps: SharedAppBarVariantProps;
13
15
  setLeftWidth: (width: number) => void;
14
16
  setRightWidth: (width: number) => void;
@@ -7,6 +7,7 @@ export {
7
7
  AppBarSlot,
8
8
  AppBarSubtitle,
9
9
  AppBarTitle,
10
+ type AppBarEdge,
10
11
  type AppBarIconButtonProps,
11
12
  type AppBarLeftProps,
12
13
  type AppBarMainProps,
@@ -1,8 +1,9 @@
1
1
  import type { AppBarVariantProps } from "@seed-design/lynx-css/recipes/app-bar";
2
2
  import type { AppBarMainVariantProps } from "@seed-design/lynx-css/recipes/app-bar-main";
3
+ import { topNavigation as topNavigationVars } from "@seed-design/lynx-css/vars/component";
3
4
  import * as React from "@lynx-js/react";
4
5
 
5
- import { useSafeArea } from "../../hooks/useSafeArea";
6
+ import { type UseSafeAreaReturn, useSafeArea } from "../../hooks/useSafeArea";
6
7
  import type { LynxViewProps } from "../../types";
7
8
  import type { AppBarContextValue, SharedAppBarVariantProps } from "./context";
8
9
 
@@ -14,10 +15,8 @@ type AppBarTheme = NonNullable<AppBarVariantProps["theme"]>;
14
15
  type LayoutChangeHandler = NonNullable<LynxViewProps["bindlayoutchange"]>;
15
16
  type AppBarStyleObject = Record<string, string | number>;
16
17
 
17
- const APP_BAR_HEIGHT_BY_THEME: Record<AppBarTheme, string> = {
18
- cupertino: "44px",
19
- android: "56px",
20
- };
18
+ // Mirrors the recipe's `dimension.x4` as a literal: Lynx drops an inline `calc()` that contains `var()`.
19
+ const ROOT_PADDING_X = 16;
21
20
 
22
21
  function getDefaultAppBarTheme(): AppBarTheme {
23
22
  const globalSystemInfo = (globalThis as typeof globalThis & { SystemInfo?: LynxSystemInfo })
@@ -38,31 +37,38 @@ export function getLayoutWidth(event: Parameters<LayoutChangeHandler>[0]): numbe
38
37
  return Math.max(0, nextWidth);
39
38
  }
40
39
 
40
+ // The left/right areas sit inside the root padding, so the title clears that padding plus the wider area.
41
41
  function getCenteredTitlePadding(leftWidth: number, rightWidth: number): string {
42
- return `${Math.max(leftWidth, rightWidth)}px`;
42
+ return `${ROOT_PADDING_X + Math.max(leftWidth, rightWidth)}px`;
43
43
  }
44
44
 
45
- function getRootLayoutStyle(theme: AppBarTheme, safeAreaInsetTop: string): AppBarStyleObject {
45
+ function getRootLayoutStyle(safeArea: UseSafeAreaReturn): AppBarStyleObject {
46
46
  return {
47
- height: `calc(${APP_BAR_HEIGHT_BY_THEME[theme]} + ${safeAreaInsetTop})`,
48
- paddingTop: safeAreaInsetTop,
47
+ height: `calc(${topNavigationVars.base.enabled.root.height} + ${safeArea.safeAreaInsetTop})`,
48
+ paddingTop: safeArea.safeAreaInsetTop,
49
+ paddingLeft: `calc(${ROOT_PADDING_X}px + ${safeArea.safeAreaInsetLeft})`,
50
+ paddingRight: `calc(${ROOT_PADDING_X}px + ${safeArea.safeAreaInsetRight})`,
49
51
  };
50
52
  }
51
53
 
54
+ // The title clears the same distance past each side's inset, so it stays centered in the safe area.
52
55
  export function getMainLayoutStyle(
53
56
  theme: AppBarMainVariantProps["theme"],
54
- safeAreaInsetTop: string,
57
+ safeArea: UseSafeAreaReturn,
58
+ centeredTitlePaddingX: string,
55
59
  ): AppBarStyleObject | undefined {
56
60
  if (theme !== "cupertino") return undefined;
57
61
 
58
62
  return {
59
- top: safeAreaInsetTop,
63
+ top: safeArea.safeAreaInsetTop,
60
64
  bottom: "0px",
65
+ paddingLeft: `calc(${safeArea.safeAreaInsetLeft} + ${centeredTitlePaddingX})`,
66
+ paddingRight: `calc(${safeArea.safeAreaInsetRight} + ${centeredTitlePaddingX})`,
61
67
  };
62
68
  }
63
69
 
64
70
  export function useAppBar(variantProps: AppBarVariantProps) {
65
- const { safeAreaInsetTop } = useSafeArea();
71
+ const safeArea = useSafeArea();
66
72
  const [leftWidth, setLeftWidth] = React.useState(0);
67
73
  const [rightWidth, setRightWidth] = React.useState(0);
68
74
  const resolvedTheme = variantProps.theme ?? getDefaultAppBarTheme();
@@ -72,7 +78,7 @@ export function useAppBar(variantProps: AppBarVariantProps) {
72
78
  theme: resolvedTheme,
73
79
  };
74
80
  const centeredTitlePaddingX = getCenteredTitlePadding(leftWidth, rightWidth);
75
- const rootLayoutStyle = getRootLayoutStyle(resolvedTheme, safeAreaInsetTop);
81
+ const rootLayoutStyle = getRootLayoutStyle(safeArea);
76
82
  const sharedVariantProps = React.useMemo<SharedAppBarVariantProps>(
77
83
  () => ({
78
84
  theme: resolvedVariantProps.theme,
@@ -84,12 +90,12 @@ export function useAppBar(variantProps: AppBarVariantProps) {
84
90
  const contextValue = React.useMemo<AppBarContextValue>(
85
91
  () => ({
86
92
  centeredTitlePaddingX,
87
- safeAreaInsetTop,
93
+ safeArea,
88
94
  sharedVariantProps,
89
95
  setLeftWidth,
90
96
  setRightWidth,
91
97
  }),
92
- [centeredTitlePaddingX, safeAreaInsetTop, sharedVariantProps],
98
+ [centeredTitlePaddingX, safeArea, sharedVariantProps],
93
99
  );
94
100
 
95
101
  return {