@vvedantb/native-ui 0.13.0 → 0.17.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/README.md +40 -19
- package/package.json +36 -3
- package/src/BottomSheetContainer.tsx +2 -2
- package/src/BulkActionBar.tsx +240 -105
- package/src/Button.tsx +11 -7
- package/src/ConfirmDialog.tsx +25 -35
- package/src/Dialog.tsx +47 -35
- package/src/DividedList.tsx +11 -7
- package/src/DropdownMenu.tsx +22 -8
- package/src/FloatingSurface.tsx +5 -2
- package/src/FloatingTabBar.tsx +13 -22
- package/src/HeaderIconButton.tsx +4 -3
- package/src/Input.tsx +2 -2
- package/src/LoadingState.tsx +132 -0
- package/src/PageTitle.tsx +9 -2
- package/src/Pager.tsx +62 -0
- package/src/PasswordInput.tsx +1 -1
- package/src/PillTabs.tsx +144 -34
- package/src/Progress.tsx +9 -7
- package/src/Select.tsx +4 -4
- package/src/Sheet.tsx +29 -22
- package/src/SheetRow.tsx +27 -9
- package/src/Skeleton.tsx +91 -9
- package/src/SurfaceRow.tsx +111 -0
- package/src/SwipeableRow.tsx +43 -0
- package/src/Switch.tsx +6 -4
- package/src/Tabs.tsx +68 -13
- package/src/Toast.tsx +107 -65
- package/src/ai/activity-step-detail.tsx +1 -3
- package/src/ai/activity-step-label.ts +981 -0
- package/src/ai/activity-step-types.ts +107 -0
- package/src/ai/activity-tasks-utils.ts +199 -0
- package/src/ai/activity-timeline.ts +45 -0
- package/src/ai/attachments.tsx +3 -1
- package/src/ai/chat-text.ts +13 -7
- package/src/ai/chat-thread.tsx +34 -27
- package/src/ai/choice-question.tsx +154 -35
- package/src/ai/code-block.tsx +189 -49
- package/src/ai/collapsible.tsx +19 -13
- package/src/ai/conversation.tsx +390 -57
- package/src/ai/icon-button.tsx +41 -6
- package/src/ai/index.ts +38 -1
- package/src/ai/message-action-sheet.tsx +4 -1
- package/src/ai/message.tsx +60 -31
- package/src/ai/prompt-input-menu.tsx +197 -0
- package/src/ai/prompt-input.tsx +301 -75
- package/src/ai/shimmer.tsx +4 -6
- package/src/ai/trace.tsx +54 -17
- package/src/ai/unified-diff.tsx +182 -0
- package/src/auth/GoogleSignInButton.tsx +6 -3
- package/src/auth/SignInScreen.tsx +11 -2
- package/src/brand/ProviderIcon.tsx +136 -0
- package/src/brand/index.ts +2 -0
- package/src/configureTheme.ts +39 -11
- package/src/controlStyles.ts +46 -30
- package/src/entries/gesture-handler.ts +4 -0
- package/src/entries/keyboard-controller.ts +4 -0
- package/src/entries/markdown.ts +1 -1
- package/src/entries/primitives.ts +24 -3
- package/src/entries/theme.ts +2 -1
- package/src/entries/video.ts +4 -0
- package/src/entries/webview.ts +6 -0
- package/src/floating.ts +25 -4
- package/src/haptics.ts +30 -4
- package/src/highlight.ts +302 -1
- package/src/index.ts +27 -5
- package/src/loadingGrid.ts +75 -0
- package/src/markdown/Markdown.tsx +234 -32
- package/src/markdown/MarkdownTable.tsx +188 -0
- package/src/markdown/tableText.ts +58 -0
- package/src/marketing/UpgradeToProCard.tsx +4 -3
- package/src/media/ImageGallery.tsx +224 -0
- package/src/media/MediaCard.tsx +100 -0
- package/src/media/imageGrid.ts +51 -0
- package/src/motion.ts +33 -0
- package/src/pagerSwipe.ts +102 -0
- package/src/palette.ts +8 -1
- package/src/primitives/button.tsx +4 -1
- package/src/primitives/keyboard-controller.tsx +106 -0
- package/src/primitives/keyboard-parts.tsx +104 -0
- package/src/primitives/overlay.tsx +133 -45
- package/src/primitives/pager-gesture.tsx +131 -0
- package/src/primitives/sheet-drag.tsx +305 -0
- package/src/primitives/sheet-gesture.tsx +181 -0
- package/src/primitives/swipe-row-gesture.tsx +290 -0
- package/src/primitives/toast-drag-gesture.tsx +92 -0
- package/src/primitives/web-click-guard.ts +50 -0
- package/src/relativeTime.ts +94 -0
- package/src/router/RouterStackPageHeader.tsx +2 -2
- package/src/settings/SettingsEmptyState.tsx +20 -10
- package/src/settings/SettingsField.tsx +11 -5
- package/src/settings/SettingsSection.tsx +8 -5
- package/src/settings/SettingsToggleRow.tsx +55 -22
- package/src/settings/relaxedSizeStyle.ts +11 -0
- package/src/sheetDrag.ts +67 -0
- package/src/swipeRow.ts +169 -0
- package/src/tabBarRoutes.ts +18 -0
- package/src/tabIndicator.ts +71 -0
- package/src/tailwind-preset.cjs +13 -6
- package/src/tailwind-preset.d.cts +5 -0
- package/src/theme.tsx +6 -2
- package/src/toastDrag.ts +58 -0
- package/src/typography.ts +20 -3
- package/src/useScheme.ts +10 -1
- package/src/video/InlineVideo.tsx +91 -0
- package/src/webview/HtmlView.tsx +91 -0
- package/src/webview/HtmlView.web.tsx +21 -0
- package/src/webview/htmlView.ts +45 -0
package/src/controlStyles.ts
CHANGED
|
@@ -133,23 +133,26 @@ const TEXT_SIZE = {
|
|
|
133
133
|
|
|
134
134
|
export type TextSize = keyof typeof TEXT_SIZE;
|
|
135
135
|
|
|
136
|
-
/** Tailwind `leading-tight` and `leading-
|
|
137
|
-
const LEADING = { tight: 1.25, snug: 1.375 } as const;
|
|
136
|
+
/** Tailwind `leading-tight`, `leading-snug` and `leading-relaxed`, as multiples of the size. */
|
|
137
|
+
const LEADING = { tight: 1.25, snug: 1.375, relaxed: 1.625 } as const;
|
|
138
|
+
|
|
139
|
+
export type TextLeading = keyof typeof LEADING;
|
|
138
140
|
|
|
139
141
|
/**
|
|
140
142
|
* Font size and line height of a `text-*` class as a style, set beside the
|
|
141
143
|
* class. On Expo web at 390 wide (0.3.6 gallery), `Button` labels with
|
|
142
144
|
* `text-sm` measured 16, so the class alone does not always reach the text.
|
|
143
|
-
* `"tight"` and `"
|
|
144
|
-
*
|
|
145
|
+
* `"tight"`, `"snug"` and `"relaxed"` are the Tailwind `leading-tight` (1.25 ×
|
|
146
|
+
* the size), `leading-snug` (1.375 ×) and `leading-relaxed` (1.625 ×, the web
|
|
147
|
+
* `.markdown` body and the settings descriptions).
|
|
145
148
|
*/
|
|
146
|
-
export function sizeStyle(size: TextSize, leading?:
|
|
149
|
+
export function sizeStyle(size: TextSize, leading?: TextLeading) {
|
|
147
150
|
const { fontSize, lineHeight } = TEXT_SIZE[size];
|
|
148
151
|
return { fontSize, lineHeight: leading ? fontSize * LEADING[leading] : lineHeight };
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
/** Tailwind `tracking-tight`, in em. */
|
|
152
|
-
const TRACKING = { tight: -0.025 } as const;
|
|
154
|
+
/** Tailwind `tracking-tight` and `tracking-wide`, in em. */
|
|
155
|
+
const TRACKING = { tight: -0.025, wide: 0.025 } as const;
|
|
153
156
|
|
|
154
157
|
/**
|
|
155
158
|
* Letter spacing of a `tracking-*` class at `fontSize`, as a style in points.
|
|
@@ -313,23 +316,26 @@ export function tabColors(variant: TabsVariant, selected: boolean, scheme: Schem
|
|
|
313
316
|
export type TabsSize = "default" | "sm";
|
|
314
317
|
|
|
315
318
|
/**
|
|
316
|
-
* Trigger box and type for `TabsTrigger` and `PillTabs`.
|
|
317
|
-
*
|
|
318
|
-
* high
|
|
319
|
-
*
|
|
320
|
-
* text
|
|
321
|
-
*
|
|
319
|
+
* Trigger box and type for `TabsTrigger` and `PillTabs`. Phone-tuned from the
|
|
320
|
+
* web look: `pills` and `segmented` are the web `.t-tab` (`py-1`, `text-sm`)
|
|
321
|
+
* grown to 36 high with `px-3.5`, so the 4 slop above and below makes 44;
|
|
322
|
+
* `sm` is the web `sm` list (`px-2.5 py-1 text-xs`) with the label kept at 14
|
|
323
|
+
* (`text-sm`, `px-3`) and also 36 high; `soft` is the web soft trigger
|
|
324
|
+
* (`px-4 py-2 text-sm`). With larger text they are the 0.3.4 sizes.
|
|
325
|
+
* `minHeight` is a minimum, so a large Dynamic Type size grows the trigger
|
|
326
|
+
* instead of clipping its label. `letterSpacing` is the web type-scale
|
|
327
|
+
* tracking of the label (`tokens/src/css/type-scale.css`): `text-sm` is 0em.
|
|
322
328
|
*/
|
|
323
329
|
export function tabTriggerSize(variant: TabsVariant, size: TabsSize, largerText: boolean) {
|
|
324
330
|
if (largerText) {
|
|
325
331
|
return size === "sm"
|
|
326
|
-
? { minHeight: 48, className: "px-4 py-2", textClassName: "text-lg" }
|
|
327
|
-
: { minHeight: 56, className: "px-4 py-3", textClassName: "text-xl" };
|
|
332
|
+
? { minHeight: 48, className: "px-4 py-2", textClassName: "text-lg", letterSpacing: 0 }
|
|
333
|
+
: { minHeight: 56, className: "px-4 py-3", textClassName: "text-xl", letterSpacing: 0 };
|
|
328
334
|
}
|
|
329
|
-
if (size === "sm") return { minHeight:
|
|
335
|
+
if (size === "sm") return { minHeight: 36, className: "px-3 py-1", textClassName: "text-sm", letterSpacing: 0 };
|
|
330
336
|
return variant === "soft"
|
|
331
|
-
? { minHeight: 36, className: "px-4 py-2", textClassName: "text-sm" }
|
|
332
|
-
: { minHeight:
|
|
337
|
+
? { minHeight: 36, className: "px-4 py-2", textClassName: "text-sm", letterSpacing: 0 }
|
|
338
|
+
: { minHeight: 36, className: "px-3.5 py-1", textClassName: "text-sm", letterSpacing: 0 };
|
|
333
339
|
}
|
|
334
340
|
|
|
335
341
|
/** The segmented track, web `--tabs-bar-bg` (`--default`). */
|
|
@@ -341,14 +347,14 @@ export function tabTrackStyle(scheme: Scheme) {
|
|
|
341
347
|
* Fill and label of the `FloatingTabBar` active pill. On the web the raised
|
|
342
348
|
* `--segment` pill sits on a muted `--default` track. The floating bar is
|
|
343
349
|
* already the raised surface (`tokens.color.fill`, near-white in light), so in
|
|
344
|
-
* light the pill takes the muted track fill (
|
|
345
|
-
* `--segment` pill would not show on it. In dark it keeps `--segment
|
|
346
|
-
*
|
|
350
|
+
* light the pill takes the muted track fill (`--default`), as a white
|
|
351
|
+
* `--segment` pill would not show on it. In dark it keeps the kit `--segment`,
|
|
352
|
+
* lighter than the dark glass. Both are `palette.tabBarPill`. The label is the
|
|
347
353
|
* segmented `--tabs-text-active`, as `tabColors("segmented", true, scheme)`.
|
|
348
354
|
*/
|
|
349
355
|
export function floatingTabPillColors(scheme: Scheme): TabColors {
|
|
350
356
|
return {
|
|
351
|
-
backgroundColor:
|
|
357
|
+
backgroundColor: palette.tabBarPill[scheme],
|
|
352
358
|
color: tabColors("segmented", true, scheme).color,
|
|
353
359
|
};
|
|
354
360
|
}
|
|
@@ -372,16 +378,26 @@ export function checkboxColors(checked: boolean, scheme: Scheme) {
|
|
|
372
378
|
};
|
|
373
379
|
}
|
|
374
380
|
|
|
381
|
+
export type SwitchSize = "sm" | "md";
|
|
382
|
+
|
|
375
383
|
/**
|
|
376
|
-
* Track and thumb size classes,
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
384
|
+
* Track and thumb size classes, the thumb offset off and on, and the track
|
|
385
|
+
* size in points for the hit slop. `justify-center` on the track centres the
|
|
386
|
+
* thumb. Without larger text:
|
|
387
|
+
* - `sm` is the web `Switch`: a `h-5 w-9` track (20 × 36, with a 1 border)
|
|
388
|
+
* and a `size-4` thumb that moves `translate-x-4`.
|
|
389
|
+
* - `md` is the larger app switch some web apps set through `UiConfig.Switch`:
|
|
390
|
+
* a `h-6 w-10` track (24 × 40, with a 1 border) and a `size-5` thumb that
|
|
391
|
+
* sits at `translate-x-0.5` and moves to `translate-x-[18px]`.
|
|
392
|
+
* With larger text both are the 0.3.4 size.
|
|
380
393
|
*/
|
|
381
|
-
export function switchSize(largerText: boolean) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
394
|
+
export function switchSize(largerText: boolean, size: SwitchSize = "sm") {
|
|
395
|
+
if (largerText) {
|
|
396
|
+
return { track: "h-8 w-14", thumb: "h-7 w-7", off: "translate-x-0", on: "translate-x-6", width: 56, height: 32 };
|
|
397
|
+
}
|
|
398
|
+
return size === "md"
|
|
399
|
+
? { track: "h-6 w-10", thumb: "h-5 w-5", off: "translate-x-0.5", on: "translate-x-[18px]", width: 40, height: 24 }
|
|
400
|
+
: { track: "h-5 w-9", thumb: "h-4 w-4", off: "translate-x-0", on: "translate-x-4", width: 36, height: 20 };
|
|
385
401
|
}
|
|
386
402
|
|
|
387
403
|
/**
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// `@vvedantb/native-ui/gesture-handler`. Imports `react-native-gesture-handler`
|
|
2
|
+
// (an optional peer): the only entry that does, so an app without it bundles
|
|
3
|
+
// the `PanResponder` sheets as before.
|
|
4
|
+
export { GestureHandlerProvider } from "../primitives/sheet-gesture";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// `@vvedantb/native-ui/keyboard-controller`. Imports
|
|
2
|
+
// `react-native-keyboard-controller` (an optional peer): the only entry that
|
|
3
|
+
// does, so an app without it keeps React Native's `KeyboardAvoidingView`.
|
|
4
|
+
export { KeyboardControllerProvider } from "../primitives/keyboard-controller";
|
package/src/entries/markdown.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// `.markdown` block. Not on the root entry: it imports the optional peer
|
|
3
3
|
// `react-native-marked`, and Metro does not tree-shake.
|
|
4
4
|
export { Markdown, markdownStyles, inlineCodeStyle, MARKDOWN_SCALE, MARKDOWN_LIST } from "../markdown/Markdown";
|
|
5
|
-
export type { MarkdownProps } from "../markdown/Markdown";
|
|
5
|
+
export type { MarkdownHeadings, MarkdownProps } from "../markdown/Markdown";
|
|
@@ -42,16 +42,24 @@ export type { IconButtonProps, IconButtonSize, IconButtonVariant } from "../Icon
|
|
|
42
42
|
export { CaptureButton } from "../CaptureButton";
|
|
43
43
|
export type { CaptureButtonProps } from "../CaptureButton";
|
|
44
44
|
export { Separator } from "../Separator";
|
|
45
|
+
export { KeyboardAvoidingView } from "../primitives/keyboard-parts";
|
|
46
|
+
export type { KeyboardAvoidingViewProps } from "../primitives/keyboard-parts";
|
|
45
47
|
export type { SeparatorProps } from "../Separator";
|
|
46
48
|
export { Progress } from "../Progress";
|
|
47
49
|
export type { ProgressProps } from "../Progress";
|
|
48
50
|
export { Spinner, CenteredSpinner } from "../Spinner";
|
|
49
51
|
export type { SpinnerProps } from "../Spinner";
|
|
52
|
+
export { LoadingState } from "../LoadingState";
|
|
53
|
+
export type { LoadingStateProps, LoadingStateSize } from "../LoadingState";
|
|
50
54
|
export { Avatar, AvatarImage, AvatarFallback } from "../Avatar";
|
|
51
55
|
export type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from "../Avatar";
|
|
52
56
|
export { initials } from "../initials";
|
|
57
|
+
export { formatCompactRelativeTime } from "../relativeTime";
|
|
58
|
+
export type { CompactRelativeTimeOptions } from "../relativeTime";
|
|
53
59
|
export { Chip } from "../Chip";
|
|
54
60
|
export { PillTabs } from "../PillTabs";
|
|
61
|
+
export { Pager } from "../Pager";
|
|
62
|
+
export type { PagerProps } from "../Pager";
|
|
55
63
|
export { Tabs, TabsList, TabsTrigger, TabsContent } from "../Tabs";
|
|
56
64
|
export type {
|
|
57
65
|
TabsProps,
|
|
@@ -61,9 +69,21 @@ export type {
|
|
|
61
69
|
TabsContentProps,
|
|
62
70
|
} from "../Tabs";
|
|
63
71
|
export { ListRow } from "../ListRow";
|
|
64
|
-
export { DividedList } from "../DividedList";
|
|
72
|
+
export { DividedList, ListDivider } from "../DividedList";
|
|
65
73
|
export type { DividedListProps } from "../DividedList";
|
|
66
74
|
export type { ListRowProps } from "../ListRow";
|
|
75
|
+
export { SwipeableRow } from "../SwipeableRow";
|
|
76
|
+
export type { SwipeableRowProps } from "../SwipeableRow";
|
|
77
|
+
export { closeSwipeRows, swipeActionsAccessibility } from "../swipeRow";
|
|
78
|
+
export type { SwipeAction } from "../swipeRow";
|
|
79
|
+
export { ImageGallery, ImageViewer } from "../media/ImageGallery";
|
|
80
|
+
export type { ImageGalleryProps, ImageViewerProps } from "../media/ImageGallery";
|
|
81
|
+
export { MediaCard } from "../media/MediaCard";
|
|
82
|
+
export type { MediaCardProps } from "../media/MediaCard";
|
|
83
|
+
export { imageAspect, imageGridTiles, imageViewerPage, MEDIA } from "../media/imageGrid";
|
|
84
|
+
export type { GalleryImage, ImageGridTile } from "../media/imageGrid";
|
|
85
|
+
export { SurfaceRow, SURFACE_ROW } from "../SurfaceRow";
|
|
86
|
+
export type { SurfaceRowProps } from "../SurfaceRow";
|
|
67
87
|
export { HeaderIconButton } from "../HeaderIconButton";
|
|
68
88
|
export { ThemeToggle } from "../ThemeToggle";
|
|
69
89
|
export type { ThemeToggleProps } from "../ThemeToggle";
|
|
@@ -76,7 +96,8 @@ export {
|
|
|
76
96
|
CardFooter,
|
|
77
97
|
} from "../Card";
|
|
78
98
|
export type { CardProps } from "../Card";
|
|
79
|
-
export { Skeleton, CardSkeleton } from "../Skeleton";
|
|
99
|
+
export { Skeleton, CardSkeleton, SkeletonList, SkeletonRow } from "../Skeleton";
|
|
100
|
+
export type { SkeletonListProps, SkeletonRowProps } from "../Skeleton";
|
|
80
101
|
export { EmptyState } from "../EmptyState";
|
|
81
102
|
export type { EmptyStateVariant } from "../EmptyState";
|
|
82
103
|
export {
|
|
@@ -105,7 +126,7 @@ export {
|
|
|
105
126
|
export type { AlertDialogProps, AlertDialogContentProps } from "../AlertDialog";
|
|
106
127
|
export { ConfirmDialog } from "../ConfirmDialog";
|
|
107
128
|
export { BulkActionBar, BULK_BAR_LABEL_MIN_WIDTH } from "../BulkActionBar";
|
|
108
|
-
export type { BulkActionBarProps, BulkBarAction } from "../BulkActionBar";
|
|
129
|
+
export type { BulkActionBarProps, BulkBarAction, BulkBarPlacement } from "../BulkActionBar";
|
|
109
130
|
export {
|
|
110
131
|
DropdownMenu,
|
|
111
132
|
DropdownMenuTrigger,
|
package/src/entries/theme.ts
CHANGED
|
@@ -13,5 +13,6 @@ export { palette, withAlpha } from "../palette";
|
|
|
13
13
|
export type { Palette } from "../palette";
|
|
14
14
|
export { configureTheme } from "../configureTheme";
|
|
15
15
|
export type { ThemeConfig, ThemeColors, ThemePalette } from "../configureTheme";
|
|
16
|
-
export { fontClass } from "../typography";
|
|
16
|
+
export { fontClass, fontFamily } from "../typography";
|
|
17
|
+
export type { ThemeFonts } from "../typography";
|
|
17
18
|
export { PINK_50, PINK_100, PINK_600, PINK_700, NEUTRAL_100, NEUTRAL_500, NEUTRAL_900, Colors } from "../colors";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// `@vvedantb/native-ui/video`. Imports `expo-video` (an optional peer): the
|
|
2
|
+
// only entry that does, so an app without it bundles no video player.
|
|
3
|
+
export { InlineVideo, INLINE_VIDEO_ASPECT } from "../video/InlineVideo";
|
|
4
|
+
export type { InlineVideoProps } from "../video/InlineVideo";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// `@vvedantb/native-ui/webview`. Imports `react-native-webview` (an optional
|
|
2
|
+
// peer): the only entry that does, so an app without it bundles no WebView.
|
|
3
|
+
export { HtmlView } from "../webview/HtmlView";
|
|
4
|
+
export type { HtmlViewProps } from "../webview/HtmlView";
|
|
5
|
+
export { HTML_VIEW_HEIGHT_SCRIPT, htmlViewNavigation, htmlViewPostScript, parseHtmlViewHeight } from "../webview/htmlView";
|
|
6
|
+
export type { HtmlViewNavigation } from "../webview/htmlView";
|
package/src/floating.ts
CHANGED
|
@@ -48,9 +48,30 @@ export const floatingFill = tokens.color.fill;
|
|
|
48
48
|
/** The `tokens.color.surface` pair itself, so `configureTheme` reaches it. */
|
|
49
49
|
export const floatingSolidFill = tokens.color.surface;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Where `FloatingTabBar` sits: `FLOATING_INSET` in from the screen sides (and,
|
|
53
|
+
* in landscape, the notch) and above the home indicator. `BulkActionBar` with
|
|
54
|
+
* `placement="tabBar"` takes the same slot while the tab bar hides.
|
|
55
|
+
*/
|
|
56
|
+
export function tabBarSlotStyle(insets: { left: number; right: number; bottom: number }): {
|
|
57
|
+
position: "absolute";
|
|
58
|
+
left: number;
|
|
59
|
+
right: number;
|
|
60
|
+
bottom: number;
|
|
61
|
+
} {
|
|
62
|
+
return {
|
|
63
|
+
position: "absolute",
|
|
64
|
+
left: insets.left + FLOATING_INSET,
|
|
65
|
+
right: insets.right + FLOATING_INSET,
|
|
66
|
+
bottom: FLOATING_INSET + Math.max(insets.bottom, 8),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The height of `FloatingTabBar`, and of `BulkActionBar` in its slot. */
|
|
71
|
+
export function tabBarHeight(largerText: boolean) {
|
|
72
|
+
return largerText ? FLOATING_TAB_BAR_HEIGHT_LARGE : FLOATING_TAB_BAR_HEIGHT;
|
|
73
|
+
}
|
|
74
|
+
|
|
51
75
|
export function tabBarReserve(largerText: boolean, bottomInset: number) {
|
|
52
|
-
|
|
53
|
-
? FLOATING_TAB_BAR_HEIGHT_LARGE
|
|
54
|
-
: FLOATING_TAB_BAR_HEIGHT;
|
|
55
|
-
return bar + FLOATING_INSET + Math.max(bottomInset, 8) + 28;
|
|
76
|
+
return tabBarHeight(largerText) + FLOATING_INSET + Math.max(bottomInset, 8) + 28;
|
|
56
77
|
}
|
package/src/haptics.ts
CHANGED
|
@@ -21,9 +21,35 @@ export function loadHaptics(): ExpoHapticsModule | null {
|
|
|
21
21
|
return expoHaptics;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function native(): ExpoHapticsModule | null {
|
|
25
|
+
return Platform.OS === "web" ? null : loadHaptics();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A value ticks past a step: a tab, a segment, a checkbox. */
|
|
24
29
|
export function selectionHaptic() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
void native()?.selectionAsync();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A physical moment: `"light"` when something snaps home (a dragged sheet
|
|
35
|
+
* springing back), `"medium"` when a destructive action fires. Fire it in the
|
|
36
|
+
* same frame as the visual, once per user action.
|
|
37
|
+
*/
|
|
38
|
+
export function impactHaptic(weight: "light" | "medium") {
|
|
39
|
+
const haptics = native();
|
|
40
|
+
if (!haptics) return;
|
|
41
|
+
void haptics.impactAsync(
|
|
42
|
+
weight === "light" ? haptics.ImpactFeedbackStyle.Light : haptics.ImpactFeedbackStyle.Medium,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An operation the user started succeeded or failed (the success and error toasts). */
|
|
47
|
+
export function notificationHaptic(outcome: "success" | "error") {
|
|
48
|
+
const haptics = native();
|
|
49
|
+
if (!haptics) return;
|
|
50
|
+
void haptics.notificationAsync(
|
|
51
|
+
outcome === "success"
|
|
52
|
+
? haptics.NotificationFeedbackType.Success
|
|
53
|
+
: haptics.NotificationFeedbackType.Error,
|
|
54
|
+
);
|
|
29
55
|
}
|
package/src/highlight.ts
CHANGED
|
@@ -111,7 +111,308 @@ const PRISM_ROLE: Record<string, CodeColorRole> = {
|
|
|
111
111
|
* "punctuation"]`) is plain.
|
|
112
112
|
*/
|
|
113
113
|
export function codeTokenColor(types: readonly string[], scheme: "light" | "dark"): string {
|
|
114
|
+
return GITHUB_CODE_COLORS[scheme][prismRole(types)];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function prismRole(types: readonly string[]): CodeColorRole {
|
|
114
118
|
let role: CodeColorRole = "plain";
|
|
115
119
|
for (const type of types) role = PRISM_ROLE[type] ?? role;
|
|
116
|
-
return
|
|
120
|
+
return role;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One coloured run of code. */
|
|
124
|
+
export type CodeToken = { content: string; role: CodeColorRole };
|
|
125
|
+
|
|
126
|
+
/** A Prism token as `prism-react-renderer` hands it over: its types, outermost first. */
|
|
127
|
+
export type PrismToken = { types: readonly string[]; content: string; empty?: boolean };
|
|
128
|
+
|
|
129
|
+
/** Languages `scriptTokens` refines: the JavaScript family, as Shiki's grammars. */
|
|
130
|
+
export const SCRIPT_LANGUAGES = new Set(["js", "jsx", "javascript", "mjs", "cjs", "ts", "tsx", "typescript", "mts", "cts"]);
|
|
131
|
+
|
|
132
|
+
/** Languages `shellTokens` colours. `prism-react-renderer` bundles no shell grammar. */
|
|
133
|
+
export const SHELL_LANGUAGES = new Set(["bash", "sh", "shell", "zsh", "shellscript", "console"]);
|
|
134
|
+
|
|
135
|
+
/** Keywords Shiki colours as constants (`constant.language`, `variable.language`, a `void` type). */
|
|
136
|
+
const SCRIPT_CONSTANT_KEYWORDS = new Set(["null", "undefined", "this", "super", "void"]);
|
|
137
|
+
|
|
138
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
139
|
+
|
|
140
|
+
/** A token's role before context: what Prism's types say, with the Shiki differences that need no context. */
|
|
141
|
+
function scriptBaseRole({ types, content }: PrismToken): CodeColorRole {
|
|
142
|
+
// Inside `${…}` Shiki keeps the punctuation in the string colour and the code in its own.
|
|
143
|
+
const interpolation = types.lastIndexOf("interpolation");
|
|
144
|
+
if (interpolation >= 0) {
|
|
145
|
+
const inner = types.slice(interpolation + 1);
|
|
146
|
+
return inner.includes("punctuation") || inner.includes("interpolation-punctuation") ? "string" : prismRole(inner);
|
|
147
|
+
}
|
|
148
|
+
// A regex character class, brackets and escapes too.
|
|
149
|
+
if (types.includes("char-class")) return "constant";
|
|
150
|
+
const role = prismRole(types);
|
|
151
|
+
if (role === "keyword" && SCRIPT_CONSTANT_KEYWORDS.has(content)) return "constant";
|
|
152
|
+
if (types.includes("builtin") && content === "console") return "plain";
|
|
153
|
+
if (types.includes("function") && content === "constructor") return "keyword";
|
|
154
|
+
return role;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type Piece = CodeToken & { line: number; types: readonly string[] };
|
|
158
|
+
|
|
159
|
+
/** Splits a token so escapes in strings and each word of plain text get a role of their own. */
|
|
160
|
+
function splitPiece(piece: Piece): Piece[] {
|
|
161
|
+
const parts =
|
|
162
|
+
piece.role === "string" && !piece.types.includes("regex")
|
|
163
|
+
? piece.content.split(/(\\.)/)
|
|
164
|
+
: piece.role === "plain"
|
|
165
|
+
? piece.content.split(/([A-Za-z_$][\w$]*)/)
|
|
166
|
+
: [piece.content];
|
|
167
|
+
return parts
|
|
168
|
+
.filter((part) => part !== "")
|
|
169
|
+
.map((part) => ({ ...piece, content: part, role: piece.role === "string" && /^\\./.test(part) ? "constant" : piece.role }));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isWord = (piece: Piece | undefined, word: string) => piece?.content === word;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Prism tokens for the JavaScript family, recoloured where Prism and the
|
|
176
|
+
* web's Shiki grammars disagree, so the phone draws the web colours:
|
|
177
|
+
* `null`, `undefined` and `this` as constants; escapes in strings; a regex
|
|
178
|
+
* character class; `${…}` punctuation in the string colour; a `const` name
|
|
179
|
+
* as a constant; parameters in the variable colour; type and class names
|
|
180
|
+
* (a capitalised word not followed by `.`) as functions; ALL_CAPS words as
|
|
181
|
+
* constants. Returns lines of runs, adjacent runs of one role merged.
|
|
182
|
+
*/
|
|
183
|
+
export function scriptTokens(lines: readonly (readonly PrismToken[])[]): CodeToken[][] {
|
|
184
|
+
const pieces = lines.flatMap((tokens, line) =>
|
|
185
|
+
tokens.filter((token) => !token.empty && token.content !== "").flatMap((token) =>
|
|
186
|
+
splitPiece({ content: token.content, types: token.types, role: scriptBaseRole(token), line }),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
// Indices of the pieces that are not whitespace.
|
|
190
|
+
const sig = pieces.flatMap((piece, index) => (piece.content.trim() === "" ? [] : [index]));
|
|
191
|
+
const at = (position: number) => {
|
|
192
|
+
const index = sig[position];
|
|
193
|
+
return index === undefined ? undefined : pieces[index];
|
|
194
|
+
};
|
|
195
|
+
const isIdentifier = (piece: Piece | undefined): piece is Piece =>
|
|
196
|
+
piece !== undefined && piece.role === "plain" && IDENTIFIER.test(piece.content);
|
|
197
|
+
const closing = (open: number) => {
|
|
198
|
+
let depth = 0;
|
|
199
|
+
for (let position = open; position < sig.length; position += 1) {
|
|
200
|
+
const content = at(position)?.content;
|
|
201
|
+
if (content === "(") depth += 1;
|
|
202
|
+
else if (content === ")") {
|
|
203
|
+
depth -= 1;
|
|
204
|
+
if (depth === 0) return position;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return undefined;
|
|
208
|
+
};
|
|
209
|
+
// An arrow function's parameters, or a declared function's or method's.
|
|
210
|
+
const isParameterList = (open: number, close: number) => {
|
|
211
|
+
const before = at(open - 1);
|
|
212
|
+
if (isWord(before, "function") || isWord(before, "constructor")) return true;
|
|
213
|
+
if (before?.role === "function" && isWord(at(open - 2), "function")) return true;
|
|
214
|
+
let depth = 0;
|
|
215
|
+
for (let position = close + 1; position < Math.min(sig.length, close + 40); position += 1) {
|
|
216
|
+
const content = at(position)?.content;
|
|
217
|
+
if (position === close + 1 && content !== ":") return content === "=>" || (content === "{" && before?.role === "function");
|
|
218
|
+
if (content === "(" || content === "<" || content === "[") depth += 1;
|
|
219
|
+
else if (content === ")" || content === ">" || content === "]") depth -= 1;
|
|
220
|
+
else if (depth === 0 && content === "=>") return true;
|
|
221
|
+
else if (depth === 0 && content === "{") return before?.role === "function";
|
|
222
|
+
else if (depth === 0 && (content === ";" || content === "=")) return false;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
};
|
|
226
|
+
for (let position = 0; position < sig.length; position += 1) {
|
|
227
|
+
if (!isWord(at(position), "(")) continue;
|
|
228
|
+
const close = closing(position);
|
|
229
|
+
if (close === undefined || !isParameterList(position, close)) continue;
|
|
230
|
+
let depth = 0;
|
|
231
|
+
let expectName = true;
|
|
232
|
+
for (let inner = position + 1; inner < close; inner += 1) {
|
|
233
|
+
const piece = at(inner);
|
|
234
|
+
const content = piece?.content;
|
|
235
|
+
if (content === "(" || content === "[" || content === "{" || content === "<") depth += 1;
|
|
236
|
+
else if (content === ")" || content === "]" || content === "}" || content === ">") depth -= 1;
|
|
237
|
+
else if (depth === 0 && content === ",") expectName = true;
|
|
238
|
+
else if (depth === 0 && (content === ":" || content === "=" || content === "?")) expectName = false;
|
|
239
|
+
else if (depth === 0 && expectName && isIdentifier(piece)) {
|
|
240
|
+
piece.role = "variable";
|
|
241
|
+
expectName = false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let inImport = false;
|
|
246
|
+
let constPattern = 0;
|
|
247
|
+
// `Map<string, number>`: Shiki draws type-argument brackets plain, Prism as operators.
|
|
248
|
+
let generic = 0;
|
|
249
|
+
for (let position = 0; position < sig.length; position += 1) {
|
|
250
|
+
const piece = at(position);
|
|
251
|
+
if (!piece) continue;
|
|
252
|
+
const previous = at(position - 1);
|
|
253
|
+
if (piece.role === "keyword" && piece.content === "import") inImport = true;
|
|
254
|
+
if (piece.role === "keyword" && piece.content === "from") inImport = false;
|
|
255
|
+
// `const { a, b } = …`: every name in the pattern.
|
|
256
|
+
if (isWord(previous, "const") && (piece.content === "{" || piece.content === "[")) constPattern = 1;
|
|
257
|
+
else if (constPattern > 0 && (piece.content === "{" || piece.content === "[")) constPattern += 1;
|
|
258
|
+
else if (constPattern > 0 && (piece.content === "}" || piece.content === "]")) constPattern -= 1;
|
|
259
|
+
if (piece.role === "keyword" && piece.content === "<" && pieces[(sig[position] ?? 0) - 1]?.role === "function") {
|
|
260
|
+
piece.role = "plain";
|
|
261
|
+
generic += 1;
|
|
262
|
+
} else if (generic > 0 && piece.role === "keyword" && piece.content === ">") {
|
|
263
|
+
piece.role = "plain";
|
|
264
|
+
generic -= 1;
|
|
265
|
+
}
|
|
266
|
+
if (!isIdentifier(piece) || inImport) continue;
|
|
267
|
+
const next = at(position + 1);
|
|
268
|
+
if ((previous?.role === "keyword" && previous.content === "const") || constPattern > 0) piece.role = "constant";
|
|
269
|
+
else if (isWord(next, "=>")) piece.role = "variable";
|
|
270
|
+
else if (/^[A-Z][A-Z0-9_]+$/.test(piece.content)) piece.role = "constant";
|
|
271
|
+
else if (/^[A-Z]/.test(piece.content) && /[a-z]/.test(piece.content) && !isWord(next, ".")) piece.role = "function";
|
|
272
|
+
}
|
|
273
|
+
return mergeLines(pieces, lines.length);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function mergeLines(pieces: ReadonlyArray<CodeToken & { line: number }>, count: number): CodeToken[][] {
|
|
277
|
+
const out: CodeToken[][] = Array.from({ length: count }, () => []);
|
|
278
|
+
for (const { content, role, line } of pieces) {
|
|
279
|
+
const runs = out[line];
|
|
280
|
+
if (!runs) continue;
|
|
281
|
+
const last = runs.at(-1);
|
|
282
|
+
if (last && last.role === role) last.content += content;
|
|
283
|
+
else runs.push({ content, role });
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Shell words Shiki colours as keywords. */
|
|
289
|
+
const SHELL_KEYWORDS = new Set([
|
|
290
|
+
"if", "then", "else", "elif", "fi", "for", "while", "until", "do", "done", "case", "esac", "in",
|
|
291
|
+
"function", "select", "break", "continue", "return", "export", "local", "readonly", "declare",
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
/** After these a new command starts. */
|
|
295
|
+
const SHELL_COMMAND_KEYWORDS = new Set(["then", "else", "do", "if", "while", "until", "elif"]);
|
|
296
|
+
|
|
297
|
+
/** Shell builtins Shiki colours as constants (`support.function.builtin`). */
|
|
298
|
+
const SHELL_BUILTINS = new Set([
|
|
299
|
+
"echo", "cd", "printf", "source", "set", "unset", "exit", "pwd", "read", "eval", "exec", "shift",
|
|
300
|
+
"trap", "wait", "type", "alias", "unalias", "true", "false", "test", "ulimit", "umask", "kill",
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Shell code in the roles of the web's Shiki `bash` grammar, for `bash`, `sh`,
|
|
305
|
+
* `shell` and `zsh` blocks: a command name as a function, its arguments as
|
|
306
|
+
* strings, `-x` and `--flag` options and numbers as constants, builtins
|
|
307
|
+
* (`echo`, `cd`) as constants, keywords, `|`, `||` and redirects as
|
|
308
|
+
* keywords, `$VAR` plain (also inside a double-quoted string), comments.
|
|
309
|
+
* `prism-react-renderer` bundles no shell grammar, so this needs no peer.
|
|
310
|
+
*/
|
|
311
|
+
export function shellTokens(code: string): CodeToken[][] {
|
|
312
|
+
return code.split("\n").map((source) => {
|
|
313
|
+
const runs: CodeToken[] = [];
|
|
314
|
+
const push = (content: string, role: CodeColorRole) => {
|
|
315
|
+
if (content === "") return;
|
|
316
|
+
const last = runs.at(-1);
|
|
317
|
+
if (last && last.role === role) last.content += content;
|
|
318
|
+
else runs.push({ content, role });
|
|
319
|
+
};
|
|
320
|
+
let index = 0;
|
|
321
|
+
let command = true;
|
|
322
|
+
// `for NAME in`: the name is plain, then `in` is a keyword.
|
|
323
|
+
let forState: "none" | "name" | "in" = "none";
|
|
324
|
+
// Inside `[ … ]` Shiki draws test operators (`-f`) as keywords and words plain.
|
|
325
|
+
let inTest = false;
|
|
326
|
+
while (index < source.length) {
|
|
327
|
+
const rest = source.slice(index);
|
|
328
|
+
const space = /^\s+/.exec(rest);
|
|
329
|
+
if (space) {
|
|
330
|
+
push(space[0], "plain");
|
|
331
|
+
index += space[0].length;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (rest.startsWith("#")) {
|
|
335
|
+
push(rest, "comment");
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (rest.startsWith('"')) {
|
|
339
|
+
// Double quotes: the string colour, with `$VAR` and `${VAR}` names plain.
|
|
340
|
+
const end = rest.slice(1).search(/(?<!\\)"/);
|
|
341
|
+
const text = end < 0 ? rest : rest.slice(0, end + 2);
|
|
342
|
+
for (const part of text.split(/(\$\{[^}]*\}|\$[A-Za-z_]\w*)/)) {
|
|
343
|
+
const braced = /^\$\{([^}]*)\}$/.exec(part);
|
|
344
|
+
if (braced) {
|
|
345
|
+
push("${", "string");
|
|
346
|
+
push(braced[1] ?? "", "plain");
|
|
347
|
+
push("}", "string");
|
|
348
|
+
} else push(part, /^\$[A-Za-z_]/.test(part) ? "plain" : "string");
|
|
349
|
+
}
|
|
350
|
+
index += text.length;
|
|
351
|
+
command = false;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (rest.startsWith("'")) {
|
|
355
|
+
const end = rest.indexOf("'", 1);
|
|
356
|
+
const text = end < 0 ? rest : rest.slice(0, end + 1);
|
|
357
|
+
push(text, "string");
|
|
358
|
+
index += text.length;
|
|
359
|
+
command = false;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const operator = /^(?:\d?>>?&?\d?|<|\|\||\||&&|;|&|\$\(|\(|\)|`)/.exec(rest);
|
|
363
|
+
if (operator) {
|
|
364
|
+
const text = operator[0];
|
|
365
|
+
// Shiki: `|`, `||` and redirects in the keyword colour, `&&` and `;` plain.
|
|
366
|
+
push(text, text === "&&" || text === ";" || text === "$(" || text === "(" || text === ")" || text === "`" ? "plain" : "keyword");
|
|
367
|
+
index += text.length;
|
|
368
|
+
command = text !== ")" && !/^\d?>/.test(text) && text !== "<";
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const variable = /^\$(?:\{[^}]*\}|[A-Za-z_]\w*|[0-9@#?$!*-])/.exec(rest);
|
|
372
|
+
if (variable) {
|
|
373
|
+
push(variable[0], "plain");
|
|
374
|
+
index += variable[0].length;
|
|
375
|
+
command = false;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const word = /^[^\s|&;<>()$"'`]+/.exec(rest)?.[0] ?? rest.charAt(0);
|
|
379
|
+
index += word.length;
|
|
380
|
+
const assignment = /^([A-Za-z_]\w*)=(.*)$/.exec(word);
|
|
381
|
+
if (SHELL_KEYWORDS.has(word) && (command || (word === "in" && forState === "in"))) {
|
|
382
|
+
push(word, "keyword");
|
|
383
|
+
command = SHELL_COMMAND_KEYWORDS.has(word);
|
|
384
|
+
forState = word === "for" ? "name" : "none";
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// A test's brackets are plain, and what follows them is not a command.
|
|
388
|
+
if (word === "[" || word === "[[" || word === "]" || word === "]]") {
|
|
389
|
+
push(word, "plain");
|
|
390
|
+
command = false;
|
|
391
|
+
inTest = word.startsWith("[");
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (inTest) {
|
|
395
|
+
push(word, /^-[a-z]+$/.test(word) ? "keyword" : "plain");
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (assignment && (command || runs.at(-2)?.content === "export")) {
|
|
399
|
+
push(assignment[1] ?? "", "plain");
|
|
400
|
+
push("=", "keyword");
|
|
401
|
+
push(assignment[2] ?? "", "plain");
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (forState === "name") {
|
|
405
|
+
push(word, "plain");
|
|
406
|
+
forState = "in";
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (command) {
|
|
410
|
+
push(word, SHELL_BUILTINS.has(word) ? "constant" : "function");
|
|
411
|
+
command = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
push(word, /^-/.test(word) || /^\d+$/.test(word) ? "constant" : "string");
|
|
415
|
+
}
|
|
416
|
+
return runs;
|
|
417
|
+
});
|
|
117
418
|
}
|