@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.
Files changed (111) hide show
  1. package/README.md +42 -21
  2. package/package.json +36 -3
  3. package/src/BottomSheetContainer.tsx +2 -2
  4. package/src/BulkActionBar.tsx +240 -105
  5. package/src/Button.tsx +11 -7
  6. package/src/ConfirmDialog.tsx +25 -35
  7. package/src/Dialog.tsx +47 -35
  8. package/src/DividedList.tsx +11 -7
  9. package/src/DropdownMenu.tsx +22 -8
  10. package/src/FloatingSurface.tsx +5 -2
  11. package/src/FloatingTabBar.tsx +13 -22
  12. package/src/HeaderIconButton.tsx +4 -3
  13. package/src/Input.tsx +2 -2
  14. package/src/LoadingState.tsx +132 -0
  15. package/src/PageTitle.tsx +9 -2
  16. package/src/Pager.tsx +62 -0
  17. package/src/PasswordInput.tsx +1 -1
  18. package/src/PillTabs.tsx +144 -34
  19. package/src/Progress.tsx +9 -7
  20. package/src/Select.tsx +4 -4
  21. package/src/Sheet.tsx +29 -22
  22. package/src/SheetRow.tsx +27 -9
  23. package/src/Skeleton.tsx +91 -9
  24. package/src/SurfaceRow.tsx +111 -0
  25. package/src/SwipeableRow.tsx +43 -0
  26. package/src/Switch.tsx +6 -4
  27. package/src/Tabs.tsx +68 -13
  28. package/src/Toast.tsx +107 -65
  29. package/src/ai/activity-step-detail.tsx +1 -3
  30. package/src/ai/activity-step-label.ts +981 -0
  31. package/src/ai/activity-step-types.ts +107 -0
  32. package/src/ai/activity-tasks-utils.ts +199 -0
  33. package/src/ai/activity-timeline.ts +45 -0
  34. package/src/ai/attachments.tsx +3 -1
  35. package/src/ai/chat-text.ts +13 -7
  36. package/src/ai/chat-thread.tsx +34 -27
  37. package/src/ai/choice-question.tsx +154 -35
  38. package/src/ai/code-block.tsx +189 -49
  39. package/src/ai/collapsible.tsx +23 -13
  40. package/src/ai/conversation.tsx +394 -57
  41. package/src/ai/icon-button.tsx +41 -6
  42. package/src/ai/index.ts +55 -4
  43. package/src/ai/message-action-sheet.tsx +4 -1
  44. package/src/ai/message.tsx +60 -31
  45. package/src/ai/prompt-input-menu.tsx +197 -0
  46. package/src/ai/prompt-input.tsx +301 -75
  47. package/src/ai/shimmer.tsx +4 -6
  48. package/src/ai/subagent-cards.tsx +84 -14
  49. package/src/ai/tool-approval-card.tsx +139 -8
  50. package/src/ai/trace.tsx +54 -17
  51. package/src/ai/unified-diff.tsx +182 -0
  52. package/src/auth/GoogleSignInButton.tsx +6 -3
  53. package/src/auth/SignInScreen.tsx +11 -2
  54. package/src/brand/ProviderIcon.tsx +136 -0
  55. package/src/brand/index.ts +2 -0
  56. package/src/configureTheme.ts +39 -11
  57. package/src/controlStyles.ts +46 -30
  58. package/src/entries/gesture-handler.ts +4 -0
  59. package/src/entries/keyboard-controller.ts +4 -0
  60. package/src/entries/markdown.ts +1 -1
  61. package/src/entries/primitives.ts +24 -3
  62. package/src/entries/theme.ts +2 -1
  63. package/src/entries/video.ts +4 -0
  64. package/src/entries/webview.ts +6 -0
  65. package/src/examples/AiChatGallery.tsx +70 -0
  66. package/src/floating.ts +25 -4
  67. package/src/haptics.ts +30 -4
  68. package/src/highlight.ts +302 -1
  69. package/src/index.ts +27 -5
  70. package/src/loadingGrid.ts +75 -0
  71. package/src/markdown/Markdown.tsx +234 -32
  72. package/src/markdown/MarkdownTable.tsx +188 -0
  73. package/src/markdown/tableText.ts +58 -0
  74. package/src/marketing/UpgradeToProCard.tsx +4 -3
  75. package/src/media/ImageGallery.tsx +224 -0
  76. package/src/media/MediaCard.tsx +100 -0
  77. package/src/media/imageGrid.ts +51 -0
  78. package/src/motion.ts +33 -0
  79. package/src/pagerSwipe.ts +102 -0
  80. package/src/palette.ts +8 -1
  81. package/src/primitives/button.tsx +4 -1
  82. package/src/primitives/keyboard-controller.tsx +106 -0
  83. package/src/primitives/keyboard-parts.tsx +104 -0
  84. package/src/primitives/overlay.tsx +133 -45
  85. package/src/primitives/pager-gesture.tsx +131 -0
  86. package/src/primitives/sheet-drag.tsx +305 -0
  87. package/src/primitives/sheet-gesture.tsx +181 -0
  88. package/src/primitives/swipe-row-gesture.tsx +290 -0
  89. package/src/primitives/toast-drag-gesture.tsx +92 -0
  90. package/src/primitives/web-click-guard.ts +50 -0
  91. package/src/relativeTime.ts +94 -0
  92. package/src/router/RouterStackPageHeader.tsx +2 -2
  93. package/src/settings/SettingsEmptyState.tsx +20 -10
  94. package/src/settings/SettingsField.tsx +11 -5
  95. package/src/settings/SettingsSection.tsx +8 -5
  96. package/src/settings/SettingsToggleRow.tsx +55 -22
  97. package/src/settings/relaxedSizeStyle.ts +11 -0
  98. package/src/sheetDrag.ts +67 -0
  99. package/src/swipeRow.ts +169 -0
  100. package/src/tabBarRoutes.ts +18 -0
  101. package/src/tabIndicator.ts +71 -0
  102. package/src/tailwind-preset.cjs +13 -6
  103. package/src/tailwind-preset.d.cts +5 -0
  104. package/src/theme.tsx +6 -2
  105. package/src/toastDrag.ts +58 -0
  106. package/src/typography.ts +20 -3
  107. package/src/useScheme.ts +10 -1
  108. package/src/video/InlineVideo.tsx +91 -0
  109. package/src/webview/HtmlView.tsx +91 -0
  110. package/src/webview/HtmlView.web.tsx +21 -0
  111. 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,
@@ -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
- const bar = largerText
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
- if (Platform.OS === "web") {
26
- return;
27
- }
28
- void loadHaptics()?.selectionAsync();
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 GITHUB_CODE_COLORS[scheme][role];
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,