@vvedantb/native-ui 0.13.0 → 0.17.1
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 +42 -21
- 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 +23 -13
- package/src/ai/conversation.tsx +394 -57
- package/src/ai/icon-button.tsx +41 -6
- package/src/ai/index.ts +55 -4
- 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/subagent-cards.tsx +84 -14
- package/src/ai/tool-approval-card.tsx +139 -8
- 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/examples/AiChatGallery.tsx +70 -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
|
@@ -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";
|
|
@@ -39,10 +39,17 @@ import {
|
|
|
39
39
|
SourcesTrigger,
|
|
40
40
|
Suggestion,
|
|
41
41
|
Suggestions,
|
|
42
|
+
SubagentResultCard,
|
|
43
|
+
SubagentRunCard,
|
|
44
|
+
ToolApprovalCard,
|
|
45
|
+
ToolDraftCard,
|
|
42
46
|
formatElapsed,
|
|
43
47
|
useStreamingDisclosure,
|
|
44
48
|
type AttachmentData,
|
|
45
49
|
type ChatStatus,
|
|
50
|
+
type DraftEdit,
|
|
51
|
+
type DraftField,
|
|
52
|
+
type SubagentStatus,
|
|
46
53
|
} from "../ai";
|
|
47
54
|
|
|
48
55
|
const SUGGESTIONS = [
|
|
@@ -73,6 +80,68 @@ const STREAMING_REASONING =
|
|
|
73
80
|
* reasoning and trace, reply, code, sources and actions), a second user
|
|
74
81
|
* message, and an assistant message that is still working while `streaming`.
|
|
75
82
|
*/
|
|
83
|
+
const DRAFT_FIELDS: DraftField[] = [
|
|
84
|
+
{ key: "to", role: "to", value: "team@example.com" },
|
|
85
|
+
{ key: "subject", role: "subject", value: "Weekly plan" },
|
|
86
|
+
{ key: "body", role: "body", value: "Here is the plan for next week." },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const RUN_STATUSES: SubagentStatus[] = ["queued", "running", "done", "failed", "cancelled", "timed_out"];
|
|
90
|
+
|
|
91
|
+
/** Tool approval, draft and subagent cards, with testIDs for E2E. */
|
|
92
|
+
export function AiCardSamples() {
|
|
93
|
+
const [decision, setDecision] = useState("none");
|
|
94
|
+
const [cancelled, setCancelled] = useState(false);
|
|
95
|
+
const decide = (name: string) => (choice: string, edits?: DraftEdit[]) =>
|
|
96
|
+
setDecision(`${name}:${choice}${edits ? `:${JSON.stringify(edits)}` : ""}`);
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<ResponseText>{`Last decision: ${decision}`}</ResponseText>
|
|
100
|
+
<ToolApprovalCard
|
|
101
|
+
testID="gallery-approval"
|
|
102
|
+
toolName="create_task"
|
|
103
|
+
args={{ title: "Review plan" }}
|
|
104
|
+
server="tasks"
|
|
105
|
+
routineMode
|
|
106
|
+
expiresLabel="tomorrow"
|
|
107
|
+
target="Project board"
|
|
108
|
+
onDecide={decide("approval")}
|
|
109
|
+
/>
|
|
110
|
+
<ToolDraftCard
|
|
111
|
+
testID="gallery-draft"
|
|
112
|
+
toolName="send_message"
|
|
113
|
+
fields={DRAFT_FIELDS}
|
|
114
|
+
server="mail"
|
|
115
|
+
expiresLabel="in 2 hours"
|
|
116
|
+
approveLabel="Send"
|
|
117
|
+
denyLabel="Discard"
|
|
118
|
+
onDecide={decide("draft")}
|
|
119
|
+
/>
|
|
120
|
+
{RUN_STATUSES.map((status) => (
|
|
121
|
+
<SubagentRunCard
|
|
122
|
+
key={status}
|
|
123
|
+
testID={`gallery-run-${status}`}
|
|
124
|
+
title="Research"
|
|
125
|
+
status={status}
|
|
126
|
+
elapsedSeconds={65}
|
|
127
|
+
statusLine={status === "running" ? "Step 2 of 4" : undefined}
|
|
128
|
+
error={status === "failed" ? "The source could not be reached." : undefined}
|
|
129
|
+
onCancel={() => setCancelled(true)}
|
|
130
|
+
/>
|
|
131
|
+
))}
|
|
132
|
+
<ResponseText>{`Cancelled: ${cancelled ? "yes" : "no"}`}</ResponseText>
|
|
133
|
+
<SubagentResultCard
|
|
134
|
+
testID="gallery-result"
|
|
135
|
+
title="Research"
|
|
136
|
+
summary="Three sources found."
|
|
137
|
+
approvalPending
|
|
138
|
+
approvalPendingIn="Planner"
|
|
139
|
+
handoffs={[{ toolName: "create_task", server: "tasks", argsSummary: '{"title":"Review plan"}' }]}
|
|
140
|
+
/>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
76
145
|
export function AiChatSamples({ streaming = true }: { streaming?: boolean }) {
|
|
77
146
|
const [feedback, setFeedback] = useState<"like" | "dislike" | null>(null);
|
|
78
147
|
const searchFold = useStreamingDisclosure(streaming);
|
|
@@ -164,6 +233,7 @@ export function AiChatSamples({ streaming = true }: { streaming?: boolean }) {
|
|
|
164
233
|
)}
|
|
165
234
|
</MessageContent>
|
|
166
235
|
</Message>
|
|
236
|
+
<AiCardSamples />
|
|
167
237
|
</>
|
|
168
238
|
);
|
|
169
239
|
}
|
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,8 @@ export { palette, withAlpha } from "./palette";
|
|
|
12
12
|
export type { Palette } from "./palette";
|
|
13
13
|
export { configureTheme } from "./configureTheme";
|
|
14
14
|
export type { ThemeConfig, ThemeColors, ThemePalette } from "./configureTheme";
|
|
15
|
-
export { fontClass } from "./typography";
|
|
15
|
+
export { fontClass, fontFamily } from "./typography";
|
|
16
|
+
export type { ThemeFonts } from "./typography";
|
|
16
17
|
export {
|
|
17
18
|
FLOATING_INSET,
|
|
18
19
|
FLOATING_TAB_BAR_HEIGHT,
|
|
@@ -26,7 +27,7 @@ export {
|
|
|
26
27
|
floatingSolidFill,
|
|
27
28
|
tabBarReserve,
|
|
28
29
|
} from "./floating";
|
|
29
|
-
export { selectionHaptic } from "./haptics";
|
|
30
|
+
export { impactHaptic, notificationHaptic, selectionHaptic } from "./haptics";
|
|
30
31
|
export { useReduceTransparency } from "./useReduceTransparency";
|
|
31
32
|
export { FloatingSurface, GlassSheetBackground } from "./FloatingSurface";
|
|
32
33
|
export {
|
|
@@ -82,12 +83,20 @@ export type { SeparatorProps } from "./Separator";
|
|
|
82
83
|
export { Progress } from "./Progress";
|
|
83
84
|
export type { ProgressProps } from "./Progress";
|
|
84
85
|
export { Spinner, CenteredSpinner } from "./Spinner";
|
|
86
|
+
export { KeyboardAvoidingView } from "./primitives/keyboard-parts";
|
|
87
|
+
export type { KeyboardAvoidingViewProps } from "./primitives/keyboard-parts";
|
|
85
88
|
export type { SpinnerProps } from "./Spinner";
|
|
89
|
+
export { LoadingState } from "./LoadingState";
|
|
90
|
+
export type { LoadingStateProps, LoadingStateSize } from "./LoadingState";
|
|
86
91
|
export { Avatar, AvatarImage, AvatarFallback } from "./Avatar";
|
|
87
92
|
export type { AvatarProps, AvatarImageProps, AvatarFallbackProps } from "./Avatar";
|
|
88
93
|
export { initials } from "./initials";
|
|
94
|
+
export { formatCompactRelativeTime } from "./relativeTime";
|
|
95
|
+
export type { CompactRelativeTimeOptions } from "./relativeTime";
|
|
89
96
|
export { Chip } from "./Chip";
|
|
90
97
|
export { PillTabs } from "./PillTabs";
|
|
98
|
+
export { Pager } from "./Pager";
|
|
99
|
+
export type { PagerProps } from "./Pager";
|
|
91
100
|
export { Tabs, TabsList, TabsTrigger, TabsContent } from "./Tabs";
|
|
92
101
|
export type {
|
|
93
102
|
TabsProps,
|
|
@@ -97,9 +106,21 @@ export type {
|
|
|
97
106
|
TabsContentProps,
|
|
98
107
|
} from "./Tabs";
|
|
99
108
|
export { ListRow } from "./ListRow";
|
|
100
|
-
export { DividedList } from "./DividedList";
|
|
109
|
+
export { DividedList, ListDivider } from "./DividedList";
|
|
101
110
|
export type { DividedListProps } from "./DividedList";
|
|
102
111
|
export type { ListRowProps } from "./ListRow";
|
|
112
|
+
export { SwipeableRow } from "./SwipeableRow";
|
|
113
|
+
export type { SwipeableRowProps } from "./SwipeableRow";
|
|
114
|
+
export { closeSwipeRows, swipeActionsAccessibility } from "./swipeRow";
|
|
115
|
+
export type { SwipeAction } from "./swipeRow";
|
|
116
|
+
export { ImageGallery, ImageViewer } from "./media/ImageGallery";
|
|
117
|
+
export type { ImageGalleryProps, ImageViewerProps } from "./media/ImageGallery";
|
|
118
|
+
export { MediaCard } from "./media/MediaCard";
|
|
119
|
+
export type { MediaCardProps } from "./media/MediaCard";
|
|
120
|
+
export { imageAspect, imageGridTiles, imageViewerPage, MEDIA } from "./media/imageGrid";
|
|
121
|
+
export type { GalleryImage, ImageGridTile } from "./media/imageGrid";
|
|
122
|
+
export { SurfaceRow, SURFACE_ROW } from "./SurfaceRow";
|
|
123
|
+
export type { SurfaceRowProps } from "./SurfaceRow";
|
|
103
124
|
export { HeaderIconButton } from "./HeaderIconButton";
|
|
104
125
|
export { ThemeToggle } from "./ThemeToggle";
|
|
105
126
|
export type { ThemeToggleProps } from "./ThemeToggle";
|
|
@@ -112,7 +133,8 @@ export {
|
|
|
112
133
|
CardFooter,
|
|
113
134
|
} from "./Card";
|
|
114
135
|
export type { CardProps } from "./Card";
|
|
115
|
-
export { Skeleton, CardSkeleton } from "./Skeleton";
|
|
136
|
+
export { Skeleton, CardSkeleton, SkeletonList, SkeletonRow } from "./Skeleton";
|
|
137
|
+
export type { SkeletonListProps, SkeletonRowProps } from "./Skeleton";
|
|
116
138
|
export { EmptyState } from "./EmptyState";
|
|
117
139
|
export type { EmptyStateVariant } from "./EmptyState";
|
|
118
140
|
export {
|
|
@@ -141,7 +163,7 @@ export {
|
|
|
141
163
|
export type { AlertDialogProps, AlertDialogContentProps } from "./AlertDialog";
|
|
142
164
|
export { ConfirmDialog } from "./ConfirmDialog";
|
|
143
165
|
export { BulkActionBar, BULK_BAR_LABEL_MIN_WIDTH } from "./BulkActionBar";
|
|
144
|
-
export type { BulkActionBarProps, BulkBarAction } from "./BulkActionBar";
|
|
166
|
+
export type { BulkActionBarProps, BulkBarAction, BulkBarPlacement } from "./BulkActionBar";
|
|
145
167
|
export {
|
|
146
168
|
DropdownMenu,
|
|
147
169
|
DropdownMenuTrigger,
|