@lotics/ui 27.15.2 → 27.16.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.
package/AGENTS.md CHANGED
@@ -27,6 +27,14 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
27
27
  license to hand-roll.
28
28
  - **One canonical component per data role** (member → `MemberChip`, select → `OptionBadge`,
29
29
  files → `FilePreview` family, …) — the catalog's Reach-by-role outranks neighboring code.
30
+ - **Responsive layout measures the CONTAINER, not the screen.** `useContainerSize()` reports the
31
+ nearest `SizeBoundary`; wrap one around any region whose width stops tracking its parent's, and
32
+ `Dialog`/`Drawer`/popover bodies already are boundaries so their contents get the panel
33
+ rather than the region they were opened from. `useScreenSize()` answers a question about the DEVICE — is there a keyboard
34
+ worth a shortcut hint, are these touch targets — and reaching for it to decide SPACE is how a
35
+ panel a few hundred pixels wide lays its contents out for the whole display. (A component that
36
+ needs a number rather than a bucket — how many columns fit — measures its own box; `Table`
37
+ does exactly that.) → [catalog.md §Utility hooks](./docs/catalog.md).
30
38
  - **A section's ADD rides its heading row, right edge** — Add files, Add fee, New line: a
31
39
  `primary` `Button` beside `SectionHeadingTitle`, rendered empty or full. Under the rows it
32
40
  extends, the verb MOVES with the row count and vanishes off-screen on a long list; a heading is
package/docs/catalog.md CHANGED
@@ -615,9 +615,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
615
615
  - **`dots_indicator`** — `DotsIndicator`: three looping bouncing dots (`size`/`color`); the
616
616
  indeterminate "working/typing" pulse `Loading` composes; use bare beside a caption while
617
617
  an agent thinks. Determinate work → `ProgressBar`.
618
- - **`animation_fade_in`** — `AnimationFadeIn`: the mount transition — children fade (+
619
- optional `translateY` rise) into place on first render, once; the entrance polish
620
- Accordion/Timeline/Stepper/AgentRun rows use.
618
+ - **`animation_fade_in`** — `AnimationFadeIn`: the mount transition — children fade into
619
+ place on first render, once; the entrance polish Accordion/Timeline/Stepper/AgentRun
620
+ rows use. `translateY` adds the rise that reads as settling into place; `translateX`
621
+ the horizontal travel of a panel arriving from an edge (negative = from the left).
621
622
 
622
623
  ### Pickers & selection controls
623
624
 
@@ -1604,10 +1605,25 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1604
1605
 
1605
1606
  ### Utility hooks & plumbing
1606
1607
 
1607
- - **`use_screen_size`** — `useScreenSize` / `getScreenSize` / `calculateScreenSize`:
1608
- `{ small, medium, large }` width-breakpoint booleans (small < 768 ≤ medium < 1728 ≤
1609
- large); tracks width changes only, so the mobile soft keyboard (a height-only change)
1610
- never re-renders.
1608
+ - **`size_boundary`** — `SizeBoundary` / `useContainerSize`: **reach for this, not
1609
+ `useScreenSize`, for anything rendered in flow.** `useContainerSize()` answers "how much
1610
+ room do *I* have" with the same `{ small, medium, large }` booleans, measured from the
1611
+ nearest enclosing `SizeBoundary` — so a table in a 600px panel lays itself out like a
1612
+ phone, because for its purposes it is one. Wrap a `SizeBoundary` around any region whose
1613
+ width stops tracking the window (beside a dockable panel, inside a resizable rail, around
1614
+ an embedded surface); `Dialog`, `Drawer` and a popover's body already are boundaries, so their
1615
+ contents get the panel's width rather than the region they were opened from. With no boundary in the tree the
1616
+ window is the container, which is the right answer for an app inside its own frame.
1617
+ - **`container_size`** — `deriveSizeBuckets` / `ContainerSizeProvider`: the thresholds and
1618
+ the react-native-free core `SizeBoundary` feeds a measured width into. Provide a width
1619
+ directly when you already have one and don't want a measuring wrapper.
1620
+ - **`use_screen_size`** — `useScreenSize` / `getScreenSize` / `calculateScreenSize`: the
1621
+ **window**, same buckets (small < 768 ≤ medium < 1728 ≤ large). Re-renders only when the
1622
+ width crosses a threshold — a height-only change (the mobile soft keyboard) and a resize
1623
+ inside one bucket both return the identical value. Correct only for
1624
+ questions about the device rather than the space: is there a keyboard to show a shortcut
1625
+ for, are these touch-sized targets, and an overlay deciding whether it is a full-screen
1626
+ sheet. Everything spatial wants `useContainerSize`.
1611
1627
  - **`use_async_fn`** — `useAsyncFn`: wrap an async function into a manual-trigger mutation —
1612
1628
  `[run, {loading, data, error}]`, unmount-safe, the error lands in state AND rethrows; the
1613
1629
  pending-state engine for a submit/download/upload action.
@@ -69,7 +69,7 @@ import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
69
69
  import { CardSelectItem } from "@lotics/ui/card_select_item";
70
70
  import { AgentRun } from "@lotics/ui/agent_run";
71
71
  import { FollowScroll } from "@lotics/ui/follow_scroll";
72
- import { useScreenSize } from "@lotics/ui/use_screen_size";
72
+ import { useContainerSize } from "@lotics/ui/size_boundary";
73
73
  import { type SourceRef } from "@lotics/ui/sources";
74
74
  import { ChangeValueInput, Change, ChangeField, ChangeFields, ChangeReasoning, ChangeRecord, ChangeReview, ChangeReviewActions, ChangeReviewHeader, type ChangeStatus } from "@lotics/ui/change_review";
75
75
  import { CompletionState } from "@lotics/ui/completion_state";
@@ -895,7 +895,7 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418" }: { chrome?:
895
895
  const isDrawer = chrome === "drawer";
896
896
  const id = useRef(1);
897
897
  const nextId = (prefix: string) => `${prefix}_${(id.current += 1)}`;
898
- const { small } = useScreenSize();
898
+ const { small } = useContainerSize();
899
899
 
900
900
  // ── first paint = Skeleton MIRRORING the final layout, never a spinner.
901
901
  // The 700ms mock stands in for the record query a real app awaits.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "27.15.2",
3
+ "version": "27.16.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
@@ -158,6 +158,8 @@
158
158
  "./fonts.css": "./src/fonts.css",
159
159
  "./keyboard": "./src/keyboard.ts",
160
160
  "./use_screen_size": "./src/use_screen_size.ts",
161
+ "./container_size": "./src/container_size.tsx",
162
+ "./size_boundary": "./src/size_boundary.tsx",
161
163
  "./use_auto_grow_height": "./src/use_auto_grow_height.ts",
162
164
  "./use_focus_ring": "./src/use_focus_ring.ts",
163
165
  "./use_gated_press": "./src/use_gated_press.ts",
@@ -5,7 +5,7 @@ import type { AgentUIPart } from "./agent_transform";
5
5
  import { ClarifyWizard, ClarifyWizardActions, ClarifyWizardScope, type ClarifyWizardAnswer } from "./clarify_wizard";
6
6
  import { DialogScrollArea } from "./dialog";
7
7
  import { FollowScroll } from "./follow_scroll";
8
- import { useScreenSize } from "@lotics/ui/use_screen_size";
8
+ import { useContainerSize } from "@lotics/ui/size_boundary";
9
9
 
10
10
  /**
11
11
  * What this pair needs from a run — structurally the shape `useAgentRun()`
@@ -64,7 +64,7 @@ export interface AgentRunPaneProps {
64
64
  */
65
65
  export function AgentRunPane(props: AgentRunPaneProps) {
66
66
  const { run, labelForCall, renderToolOutput, onCancel } = props;
67
- const { small } = useScreenSize();
67
+ const { small } = useContainerSize();
68
68
  const pending = run.pendingChoice;
69
69
 
70
70
  // Mapped once per question set, not per render: the wizard keys its per-step
@@ -7,10 +7,14 @@ interface AnimationFadeInProps {
7
7
  /** Rise this many px while fading in — the "appearing into place" gesture.
8
8
  * Default 0 (fade only). */
9
9
  translateY?: number;
10
+ /** Travel this many px horizontally while fading in — negative enters from the
11
+ * left, positive from the right. The gesture for a panel arriving from an
12
+ * edge rather than settling into place. Default 0. */
13
+ translateX?: number;
10
14
  }
11
15
 
12
16
  export function AnimationFadeIn(props: AnimationFadeInProps) {
13
- const { children, style, translateY = 0 } = props;
17
+ const { children, style, translateY = 0, translateX = 0 } = props;
14
18
  const progress = useRef(new Animated.Value(0)).current;
15
19
 
16
20
  useEffect(() => {
@@ -21,9 +25,18 @@ export function AnimationFadeIn(props: AnimationFadeInProps) {
21
25
  }).start();
22
26
  }, [progress]);
23
27
 
24
- const transform = translateY
25
- ? [{ translateY: progress.interpolate({ inputRange: [0, 1], outputRange: [translateY, 0] }) }]
26
- : undefined;
28
+ const offset = (from: number) =>
29
+ progress.interpolate({ inputRange: [0, 1], outputRange: [from, 0] });
30
+ const transform = [
31
+ ...(translateX ? [{ translateX: offset(translateX) }] : []),
32
+ ...(translateY ? [{ translateY: offset(translateY) }] : []),
33
+ ];
27
34
 
28
- return <Animated.View style={[{ opacity: progress, transform }, style]}>{children}</Animated.View>;
35
+ return (
36
+ <Animated.View
37
+ style={[{ opacity: progress, transform: transform.length ? transform : undefined }, style]}
38
+ >
39
+ {children}
40
+ </Animated.View>
41
+ );
29
42
  }
@@ -0,0 +1,63 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+
3
+ /**
4
+ * The one set of width thresholds in the kit. `use_screen_size` measures the
5
+ * window against them and a `SizeBoundary` measures a box, so a 400px panel on a
6
+ * 4K display reads exactly like a phone — which is the point.
7
+ */
8
+ const breakpoints = {
9
+ /** Below this, a box is phone-shaped: stack, and prefer sheets to popovers. */
10
+ small: 768,
11
+ /** At or above this, a box has room to spend on density. */
12
+ large: 1728,
13
+ };
14
+
15
+ export interface SizeBuckets {
16
+ /** For most mobile phones — or any box that narrow. */
17
+ small: boolean;
18
+ /** iPad Mini through a 16" MacBook Pro — or any box that wide. */
19
+ medium: boolean;
20
+ /** Most external monitors — or any box that wide. */
21
+ large: boolean;
22
+ }
23
+
24
+ export function deriveSizeBuckets(width: number): SizeBuckets {
25
+ return {
26
+ small: width < breakpoints.small,
27
+ medium: width >= breakpoints.small && width < breakpoints.large,
28
+ large: width >= breakpoints.large,
29
+ };
30
+ }
31
+
32
+ /** Null means no enclosing boundary — the window is the container. */
33
+ export const ContainerSizeContext = createContext<SizeBuckets | null>(null);
34
+
35
+ /**
36
+ * Publishes a measured width to descendants as buckets.
37
+ *
38
+ * The value is memoized on the buckets, not the width, so a resize that does not
39
+ * cross a threshold produces no new value and re-renders no consumer. That
40
+ * matters because a subtree under one boundary can hold dozens of readers, and a
41
+ * drag on a resizable panel emits a measurement per frame.
42
+ *
43
+ * Kept free of `react-native` imports so it is unit-testable; the measuring
44
+ * wrapper that feeds it a width lives in `size_boundary`.
45
+ */
46
+ export function ContainerSizeProvider(props: { width: number; children: ReactNode }) {
47
+ const { width, children } = props;
48
+ // The two booleans, not the width, are the dependencies — that is what keeps
49
+ // the published value identical across a resize inside one bucket.
50
+ const small = width < breakpoints.small;
51
+ const large = width >= breakpoints.large;
52
+ const value = useMemo<SizeBuckets>(
53
+ () => ({ small, medium: !small && !large, large }),
54
+ [small, large],
55
+ );
56
+
57
+ return <ContainerSizeContext.Provider value={value}>{children}</ContainerSizeContext.Provider>;
58
+ }
59
+
60
+ /** The nearest boundary's buckets, or null when nothing encloses this component. */
61
+ export function useEnclosingContainerSize(): SizeBuckets | null {
62
+ return useContext(ContainerSizeContext);
63
+ }
@@ -6,7 +6,7 @@ import { CONTROL_RADIUS } from "./control_surface";
6
6
  import { MenuButton } from "./menu_button";
7
7
  import { Calendar, CalendarRangeValue, CalendarRef } from "./date_calendar";
8
8
  import { TimePicker } from "./time_picker";
9
- import { useScreenSize } from "./use_screen_size";
9
+ import { useContainerSize } from "./size_boundary";
10
10
  import { SegmentLabels } from "./date_segments";
11
11
  import { PresetId, PRESET_IDS, getPresetValue } from "./date_filter_presets";
12
12
  import { formatDate } from "./format_date";
@@ -134,7 +134,7 @@ export function DateFilter(props: DateFilterProps) {
134
134
  () => ({ ...loc, ...props.labels }),
135
135
  [loc, props.labels],
136
136
  );
137
- const screenSize = useScreenSize();
137
+ const screenSize = useContainerSize();
138
138
  const calendarRef = useRef<CalendarRef>(null);
139
139
 
140
140
  // A half-picked range is held HERE and never emitted — the same rule
package/src/dialog.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { createContext, useCallback, useContext, useMemo, useState } from "react";
2
2
  import { Animated, Modal, StyleSheet, View, ScrollView } from "react-native";
3
3
  import { useScreenSize } from "@lotics/ui/use_screen_size";
4
+ import { SizeBoundary } from "@lotics/ui/size_boundary";
4
5
  import { PortalHost } from "@lotics/ui/portal";
5
6
  import { colors } from "@lotics/ui/colors";
6
7
  import { IconButton } from "@lotics/ui/icon_button";
@@ -167,7 +168,7 @@ export function Dialog(props: DialogProps) {
167
168
  <View style={[styles.closeButtonContainer, { paddingHorizontal: screenSize.small ? 16 : 24 }]}>
168
169
  <IconButton icon="x" size="lg" accessibilityLabel={locale.overlay.close} onPress={handleClose} />
169
170
  </View>
170
- <View style={styles.container}>{children}</View>
171
+ <SizeBoundary style={styles.container}>{children}</SizeBoundary>
171
172
  </View>
172
173
  </PortalHost>
173
174
  </Animated.View>
package/src/drawer.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { ReactNode, useEffect } from "react";
2
2
  import { Modal, Pressable, StyleSheet, View } from "react-native";
3
3
  import { useScreenSize } from "@lotics/ui/use_screen_size";
4
+ import { SizeBoundary } from "@lotics/ui/size_boundary";
4
5
  import { PortalHost } from "@lotics/ui/portal";
5
6
  import { colors } from "@lotics/ui/colors";
6
7
  import { IconButton } from "@lotics/ui/icon_button";
@@ -123,9 +124,9 @@ export function Drawer(props: DrawerProps) {
123
124
  ) : null}
124
125
  <IconButton icon="x" size="lg" accessibilityLabel={loc.close} onPress={handleClose} />
125
126
  </View>
126
- <View testID={testID} style={styles.body}>
127
+ <SizeBoundary testID={testID} style={styles.body}>
127
128
  {children}
128
- </View>
129
+ </SizeBoundary>
129
130
  </PortalHost>
130
131
  </View>
131
132
  </View>
@@ -3,7 +3,7 @@ import { Text } from "@lotics/ui/text";
3
3
  import { colors } from "@lotics/ui/colors";
4
4
  import { Spacer } from "@lotics/ui/spacer";
5
5
  import { ReactNode } from "react";
6
- import { useScreenSize } from "@lotics/ui/use_screen_size";
6
+ import { useContainerSize } from "@lotics/ui/size_boundary";
7
7
 
8
8
  interface PageContentProps {
9
9
  children: ReactNode;
@@ -23,7 +23,7 @@ interface PageContentProps {
23
23
  */
24
24
  export function PageContent(props: PageContentProps) {
25
25
  const { children, title, titleRight, description, header, footer, size, fullscreen } = props;
26
- const screenSize = useScreenSize();
26
+ const screenSize = useContainerSize();
27
27
 
28
28
  const maxWidth = !screenSize.small && size ? PAGE_SIZES[size] : undefined;
29
29
 
package/src/popover.tsx CHANGED
@@ -12,6 +12,7 @@ import { colors } from "./colors";
12
12
  import { MIN_CONTROL_WIDTH } from "./control_surface";
13
13
  import { IconButton } from "./icon_button";
14
14
  import { Portal } from "./portal";
15
+ import { SizeBoundary } from "./size_boundary";
15
16
  import { Divider } from "./divider";
16
17
  import { useOverlayScope } from "./overlay_scope";
17
18
  import {
@@ -714,7 +715,7 @@ export function PopoverContent(props: PopoverContentProps) {
714
715
  )}
715
716
  {header}
716
717
  {disableBodyScroll ? (
717
- <View style={style}>{bodyChildren}</View>
718
+ <SizeBoundary style={style}>{bodyChildren}</SizeBoundary>
718
719
  ) : (
719
720
  // FULL-BLEED HORIZONTALLY, then re-inset by the same 12. The panel
720
721
  // pads all four sides, so a scroller sitting inside that padding
@@ -731,7 +732,10 @@ export function PopoverContent(props: PopoverContentProps) {
731
732
  style={[SCROLL_BODY, style]}
732
733
  contentContainerStyle={[SCROLL_BODY_CONTENT, contentContainerStyle]}
733
734
  >
734
- {bodyChildren}
735
+ {/* A popover is a box of its own — a few hundred px — so its contents
736
+ size to the panel rather than to whatever region it was opened
737
+ from. Both body paths get it; one of them is not a boundary. */}
738
+ <SizeBoundary>{bodyChildren}</SizeBoundary>
735
739
  </ScrollView>
736
740
  )}
737
741
  {footer}
@@ -0,0 +1,65 @@
1
+ import { useCallback, useState, type ReactNode } from "react";
2
+ import { View, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
3
+ import { ContainerSizeProvider, useEnclosingContainerSize, type SizeBuckets } from "./container_size";
4
+ import { useScreenSize } from "./use_screen_size";
5
+
6
+ /**
7
+ * Declares the box that descendants should size themselves against.
8
+ *
9
+ * Mount one wherever a region's width stops tracking the window — beside a
10
+ * dockable panel, inside a resizable rail, around an embedded surface — and
11
+ * never per component. A component under it asks `useContainerSize()` and gets
12
+ * the box it is actually in, so a table in a 600px column stops laying itself
13
+ * out as though it had the whole 1600px display.
14
+ *
15
+ * Until the first `onLayout` it reports the WINDOW's bucket, so the opening paint
16
+ * matches what the same tree rendered before any boundary existed. Seeding at
17
+ * zero instead would start every mount at "phone" and flash the stacked layout on
18
+ * the way out of it.
19
+ */
20
+ export function SizeBoundary(props: {
21
+ children: ReactNode;
22
+ style?: StyleProp<ViewStyle>;
23
+ testID?: string;
24
+ }) {
25
+ const { children, style, testID } = props;
26
+ const screen = useScreenSize();
27
+ const [width, setWidth] = useState<number | null>(null);
28
+
29
+ const handleLayout = useCallback((event: LayoutChangeEvent) => {
30
+ setWidth(event.nativeEvent.layout.width);
31
+ }, []);
32
+
33
+ return (
34
+ <View onLayout={handleLayout} style={style} testID={testID}>
35
+ <ContainerSizeProvider width={width ?? screenSeedWidth(screen)}>
36
+ {children}
37
+ </ContainerSizeProvider>
38
+ </View>
39
+ );
40
+ }
41
+
42
+ /** A width that lands in the same bucket the window is in, for the first frame. */
43
+ function screenSeedWidth(screen: SizeBuckets): number {
44
+ if (screen.small) return 0;
45
+ if (screen.large) return 1728;
46
+ return 768;
47
+ }
48
+
49
+ /**
50
+ * How much room this component actually has: the nearest `SizeBoundary`, or the
51
+ * window when nothing encloses it.
52
+ *
53
+ * Reach for this over `useScreenSize` for anything rendered *in flow*. The window
54
+ * is the right question only for chrome that spans it (is this a phone?) and for
55
+ * overlays, which fill the viewport regardless of what they were opened from.
56
+ *
57
+ * Falling back to the window is not a stand-in for a missing value — with no
58
+ * boundary in the tree the window IS the container, which is also why an app
59
+ * embedded in its own frame gets the right answer without mounting anything.
60
+ */
61
+ export function useContainerSize(): SizeBuckets {
62
+ const enclosing = useEnclosingContainerSize();
63
+ const screen = useScreenSize();
64
+ return enclosing ?? screen;
65
+ }
package/src/text_utils.ts CHANGED
@@ -132,7 +132,10 @@ export function getInputTextStyle(): TextStyle {
132
132
  * Returns the input line height for layout calculations.
133
133
  * Use this when you need to calculate heights for multiline inputs.
134
134
  *
135
- * @param isSmallScreen - Whether the screen is small (mobile). Get this from useScreenSize().small
135
+ * @param isSmallScreen - Whether the WINDOW is small (a phone). Input metrics are
136
+ * a touch-target question, so this comes from `useScreenSize().small` — never
137
+ * from the enclosing container, or a text field in a narrow desktop panel would
138
+ * get phone line heights.
136
139
  */
137
140
  /** {@link multilineInputHeight} at this platform's input line height. */
138
141
  export function getMultilineInputHeight(numberOfLines: number, isSmallScreen: boolean): number {
@@ -1,13 +1,6 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { Dimensions } from "react-native";
3
-
4
- const breakpoints = {
5
- // small (mobile)
6
- small: 768,
7
- // medium (tablet and laptop)
8
- large: 1728,
9
- // large (external monitor)
10
- };
3
+ import { deriveSizeBuckets } from "./container_size";
11
4
 
12
5
  export function useScreenSize() {
13
6
  // NOTE: DO NOT USE `useWindowDimensions` from `react-native`. On mobile browsers, when the soft keyboard appears, the window.innerHeight or Dimensions can change.
@@ -28,7 +21,14 @@ export function useScreenSize() {
28
21
  // This prevents re-renders when keyboards appear/disappear
29
22
  if (currentWidth !== lastKnownWidth) {
30
23
  lastKnownWidth = currentWidth; // Update the tracked width
31
- setSize(calculateScreenSize(screen));
24
+ // ...and only when the width crosses a THRESHOLD. A new object per pixel
25
+ // re-renders every consumer through a whole drag for a value that did not
26
+ // change, and `useContainerSize` calls this on every one of its consumers
27
+ // to cover the no-boundary case — so the waste multiplies.
28
+ setSize((prev) => {
29
+ const next = calculateScreenSize(screen);
30
+ return next.small === prev.small && next.large === prev.large ? prev : next;
31
+ });
32
32
  }
33
33
  });
34
34
 
@@ -41,19 +41,10 @@ export function useScreenSize() {
41
41
  return size;
42
42
  }
43
43
 
44
+ /** The window's buckets. `SizeBoundary` measures a box against the same
45
+ * thresholds — they are defined once, in `container_size`. */
44
46
  export function calculateScreenSize(dimensions: { width: number }) {
45
- const small = dimensions.width < breakpoints.small;
46
- const medium = dimensions.width >= breakpoints.small && dimensions.width < breakpoints.large;
47
- const large = dimensions.width >= breakpoints.large;
48
-
49
- return {
50
- /** For most mobile phones */
51
- small,
52
- /** For tablet and laptop displays ranging from iPad Mini to 16' MacBook Pro */
53
- medium,
54
- /** For most external monitors */
55
- large,
56
- };
47
+ return deriveSizeBuckets(dimensions.width);
57
48
  }
58
49
 
59
50
  export function getScreenSize() {