@vvedantb/native-ui 0.8.0 → 0.10.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 +145 -7
- package/package.json +35 -8
- package/src/BulkActionBar.tsx +303 -0
- package/src/DividedList.tsx +37 -0
- package/src/DropdownMenu.tsx +439 -0
- package/src/FloatingFAB.tsx +4 -1
- package/src/ai/activity-step-detail.tsx +95 -0
- package/src/ai/choice-question.tsx +297 -0
- package/src/ai/code-block.tsx +50 -7
- package/src/ai/index.ts +10 -0
- package/src/ai/message.tsx +59 -12
- package/src/ai/prompt-input.tsx +79 -28
- package/src/ai/trace.tsx +32 -1
- package/src/auth/AuthScreen.tsx +32 -4
- package/src/auth/SecondFactorScreen.tsx +87 -0
- package/src/auth/SignInScreen.tsx +262 -1
- package/src/auth/index.ts +23 -1
- package/src/auth/totp.ts +202 -0
- package/src/configureTheme.ts +60 -0
- package/src/entries/markdown.ts +5 -0
- package/src/entries/primitives.ts +22 -0
- package/src/entries/theme.ts +2 -0
- package/src/examples/ParityGallery.tsx +56 -0
- package/src/floating.ts +2 -4
- package/src/highlight.ts +117 -0
- package/src/index.ts +24 -0
- package/src/markdown/Markdown.tsx +191 -0
- package/src/push.ts +186 -0
package/src/auth/totp.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useSignIn } from "@clerk/clerk-expo";
|
|
3
|
+
import { authErrorMessage } from "./authErrors";
|
|
4
|
+
|
|
5
|
+
/** The messages the second-factor step can show. Generic: none names a provider. */
|
|
6
|
+
export const TOTP_MESSAGES = {
|
|
7
|
+
invalidCode: "Incorrect code. Check your authenticator app and try again.",
|
|
8
|
+
invalidCodeGeneric: "Incorrect code. Check it and try again.",
|
|
9
|
+
sendFailed: "Could not send the code. Try again.",
|
|
10
|
+
needsAnotherStep: "This account needs another step to sign in. Finish it on the web.",
|
|
11
|
+
notReady: "Sign in is not ready yet. Try again in a moment.",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
/** The second-factor strategies the built-in step handles. */
|
|
15
|
+
export type SecondFactorStrategy = "totp" | "backup_code" | "phone_code" | "email_code";
|
|
16
|
+
|
|
17
|
+
/** Default order: the first one the account supports wins. */
|
|
18
|
+
export const SECOND_FACTOR_PREFERENCE: readonly SecondFactorStrategy[] = [
|
|
19
|
+
"totp",
|
|
20
|
+
"email_code",
|
|
21
|
+
"phone_code",
|
|
22
|
+
"backup_code",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** One entry of `signIn.supportedSecondFactors`. */
|
|
26
|
+
export type SecondFactor = {
|
|
27
|
+
strategy: string;
|
|
28
|
+
emailAddressId?: string;
|
|
29
|
+
phoneNumberId?: string;
|
|
30
|
+
safeIdentifier?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type AttemptResult = { status: string | null; createdSessionId: string | null };
|
|
34
|
+
|
|
35
|
+
/** The part of the sign-in resource that TOTP needs. Tests pass a mock. */
|
|
36
|
+
export type TotpSignIn = {
|
|
37
|
+
attemptSecondFactor(params: { strategy: "totp"; code: string }): Promise<AttemptResult>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** The part of the sign-in resource that any second factor needs. Tests pass a mock. */
|
|
41
|
+
export type SecondFactorSignIn = {
|
|
42
|
+
supportedSecondFactors?: SecondFactor[] | null;
|
|
43
|
+
prepareSecondFactor?(params: {
|
|
44
|
+
strategy: "phone_code" | "email_code";
|
|
45
|
+
phoneNumberId?: string;
|
|
46
|
+
emailAddressId?: string;
|
|
47
|
+
}): Promise<unknown>;
|
|
48
|
+
attemptSecondFactor(params: { strategy: SecondFactorStrategy; code: string }): Promise<AttemptResult>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type TotpSetActive = (params: { session: string }) => Promise<void>;
|
|
52
|
+
|
|
53
|
+
export type TotpResult = { ok: true } | { ok: false; error: string };
|
|
54
|
+
|
|
55
|
+
/** The strategies in `factors` that the built-in step handles, in the order given. */
|
|
56
|
+
export function supportedSecondFactorStrategies(
|
|
57
|
+
factors: readonly SecondFactor[] | null | undefined,
|
|
58
|
+
): SecondFactorStrategy[] {
|
|
59
|
+
const found = new Set((factors ?? []).map((factor) => factor.strategy));
|
|
60
|
+
return SECOND_FACTOR_PREFERENCE.filter((strategy) => found.has(strategy));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The default strategy: `totp`, then `email_code`, `phone_code`, `backup_code`. `null` when none is supported. */
|
|
64
|
+
export function pickSecondFactorStrategy(
|
|
65
|
+
factors: readonly SecondFactor[] | null | undefined,
|
|
66
|
+
): SecondFactorStrategy | null {
|
|
67
|
+
return supportedSecondFactorStrategies(factors)[0] ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Sends the code for `phone_code` and `email_code`, with the `phoneNumberId` or
|
|
72
|
+
* `emailAddressId` of the matching factor. `totp` and `backup_code` need no
|
|
73
|
+
* prepare: it returns `{ ok: true }` without a call. Never throws.
|
|
74
|
+
*/
|
|
75
|
+
export async function prepareSecondFactorIfNeeded(args: {
|
|
76
|
+
signIn: Pick<SecondFactorSignIn, "supportedSecondFactors" | "prepareSecondFactor">;
|
|
77
|
+
strategy: SecondFactorStrategy;
|
|
78
|
+
factor?: SecondFactor;
|
|
79
|
+
}): Promise<TotpResult> {
|
|
80
|
+
const { signIn, strategy } = args;
|
|
81
|
+
if (strategy !== "phone_code" && strategy !== "email_code") return { ok: true };
|
|
82
|
+
if (!signIn.prepareSecondFactor) return { ok: false, error: TOTP_MESSAGES.sendFailed };
|
|
83
|
+
const factor = args.factor ?? signIn.supportedSecondFactors?.find((item) => item.strategy === strategy);
|
|
84
|
+
try {
|
|
85
|
+
if (strategy === "phone_code") {
|
|
86
|
+
await signIn.prepareSecondFactor({
|
|
87
|
+
strategy,
|
|
88
|
+
...(factor?.phoneNumberId ? { phoneNumberId: factor.phoneNumberId } : {}),
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
await signIn.prepareSecondFactor({
|
|
92
|
+
strategy,
|
|
93
|
+
...(factor?.emailAddressId ? { emailAddressId: factor.emailAddressId } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return { ok: true };
|
|
97
|
+
} catch (caught) {
|
|
98
|
+
return { ok: false, error: authErrorMessage(caught, TOTP_MESSAGES.sendFailed) };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks a second-factor code and starts the session. Call
|
|
104
|
+
* `prepareSecondFactorIfNeeded` first for `phone_code` and `email_code`.
|
|
105
|
+
* Never throws: a failure is `{ ok: false, error }`. An API error shows its
|
|
106
|
+
* own text (such as "Incorrect code") unless it names the provider; anything
|
|
107
|
+
* else gives `TOTP_MESSAGES.invalidCode` (`totp`) or `invalidCodeGeneric`.
|
|
108
|
+
*/
|
|
109
|
+
export async function submitSecondFactor(args: {
|
|
110
|
+
signIn: Pick<SecondFactorSignIn, "attemptSecondFactor">;
|
|
111
|
+
setActive: TotpSetActive;
|
|
112
|
+
code: string;
|
|
113
|
+
strategy: SecondFactorStrategy;
|
|
114
|
+
}): Promise<TotpResult> {
|
|
115
|
+
try {
|
|
116
|
+
const attempt = await args.signIn.attemptSecondFactor({ strategy: args.strategy, code: args.code });
|
|
117
|
+
if (attempt.status === "complete" && attempt.createdSessionId) {
|
|
118
|
+
await args.setActive({ session: attempt.createdSessionId });
|
|
119
|
+
return { ok: true };
|
|
120
|
+
}
|
|
121
|
+
return { ok: false, error: TOTP_MESSAGES.needsAnotherStep };
|
|
122
|
+
} catch (caught) {
|
|
123
|
+
const fallback = args.strategy === "totp" ? TOTP_MESSAGES.invalidCode : TOTP_MESSAGES.invalidCodeGeneric;
|
|
124
|
+
return { ok: false, error: authErrorMessage(caught, fallback) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Checks a TOTP code and starts the session. TOTP needs no `prepareSecondFactor`. Same as `submitSecondFactor` with `strategy: "totp"`. */
|
|
129
|
+
export function submitTotpSecondFactor(args: {
|
|
130
|
+
signIn: TotpSignIn;
|
|
131
|
+
setActive: TotpSetActive;
|
|
132
|
+
code: string;
|
|
133
|
+
}): Promise<TotpResult> {
|
|
134
|
+
return submitSecondFactor({ signIn: args.signIn, setActive: args.setActive, code: args.code, strategy: "totp" });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type UseSecondFactorOptions = {
|
|
138
|
+
/** The strategy `submit` uses until `setStrategy` changes it. Default `"totp"`. */
|
|
139
|
+
strategy?: SecondFactorStrategy;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type UseSecondFactorResult = {
|
|
143
|
+
/** Checks the code and starts the session. Never throws. Sets `error` on failure. */
|
|
144
|
+
submit: (code: string) => Promise<TotpResult>;
|
|
145
|
+
/** Sends the code for `phone_code` and `email_code` (no call for the others). Never throws. Sets `error` on failure. */
|
|
146
|
+
prepare: (strategy?: SecondFactorStrategy, factor?: SecondFactor) => Promise<TotpResult>;
|
|
147
|
+
/** The strategy `submit` uses. */
|
|
148
|
+
strategy: SecondFactorStrategy;
|
|
149
|
+
setStrategy: (strategy: SecondFactorStrategy) => void;
|
|
150
|
+
/** True while `submit` or `prepare` runs. */
|
|
151
|
+
loading: boolean;
|
|
152
|
+
/** The last failure message, or `""`. */
|
|
153
|
+
error: string;
|
|
154
|
+
clearError: () => void;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Completes a sign-in that returned `needs_second_factor`. Default strategy
|
|
159
|
+
* `"totp"` (an authenticator-app code); pass `{ strategy }` or call
|
|
160
|
+
* `setStrategy` for the others. Pair with `SecondFactorScreen`:
|
|
161
|
+
* `onSubmit={submit}`, `error={error}`, `loading={loading}`. After
|
|
162
|
+
* `{ ok: true }` the session is active: navigate in the caller.
|
|
163
|
+
*/
|
|
164
|
+
export function useSecondFactor(options: UseSecondFactorOptions = {}): UseSecondFactorResult {
|
|
165
|
+
const { isLoaded, signIn, setActive } = useSignIn();
|
|
166
|
+
const [chosen, setChosen] = useState<SecondFactorStrategy | null>(null);
|
|
167
|
+
const strategy = chosen ?? options.strategy ?? "totp";
|
|
168
|
+
const [loading, setLoading] = useState(false);
|
|
169
|
+
const [error, setError] = useState("");
|
|
170
|
+
|
|
171
|
+
const submit = async (code: string): Promise<TotpResult> => {
|
|
172
|
+
if (loading) return { ok: false, error };
|
|
173
|
+
if (!isLoaded || !signIn || !setActive) {
|
|
174
|
+
setError(TOTP_MESSAGES.notReady);
|
|
175
|
+
return { ok: false, error: TOTP_MESSAGES.notReady };
|
|
176
|
+
}
|
|
177
|
+
setLoading(true);
|
|
178
|
+
setError("");
|
|
179
|
+
const result = await submitSecondFactor({ signIn, setActive, code, strategy });
|
|
180
|
+
if (!result.ok) setError(result.error);
|
|
181
|
+
setLoading(false);
|
|
182
|
+
return result;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const prepare = async (
|
|
186
|
+
prepareStrategy: SecondFactorStrategy = strategy,
|
|
187
|
+
factor?: SecondFactor,
|
|
188
|
+
): Promise<TotpResult> => {
|
|
189
|
+
if (!isLoaded || !signIn) {
|
|
190
|
+
setError(TOTP_MESSAGES.notReady);
|
|
191
|
+
return { ok: false, error: TOTP_MESSAGES.notReady };
|
|
192
|
+
}
|
|
193
|
+
setLoading(true);
|
|
194
|
+
setError("");
|
|
195
|
+
const result = await prepareSecondFactorIfNeeded({ signIn, strategy: prepareStrategy, factor });
|
|
196
|
+
if (!result.ok) setError(result.error);
|
|
197
|
+
setLoading(false);
|
|
198
|
+
return result;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return { submit, prepare, strategy, setStrategy: setChosen, loading, error, clearError: () => setError("") };
|
|
202
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { palette, type Palette } from "./palette";
|
|
2
|
+
import { tokens, type Tokens } from "./tokens";
|
|
3
|
+
|
|
4
|
+
/** `tokens` and `palette` value types with the literal hex widened to `string` and `readonly` dropped. */
|
|
5
|
+
type Widen<T> = T extends string ? string : { -readonly [K in keyof T]: Widen<T[K]> };
|
|
6
|
+
|
|
7
|
+
/** The `tokens.color` shape: each key a `{ light, dark }` pair, except `white` and `black`. */
|
|
8
|
+
export type ThemeColors = Widen<Tokens["color"]>;
|
|
9
|
+
|
|
10
|
+
/** The `palette` shape: each key a `{ light, dark }` pair, except `destructive`. */
|
|
11
|
+
export type ThemePalette = Widen<Palette>;
|
|
12
|
+
|
|
13
|
+
export type ThemeConfig = {
|
|
14
|
+
/** Overrides for `tokens.color` (neutrals and surfaces). Keys not given keep the package value. */
|
|
15
|
+
color?: Partial<ThemeColors>;
|
|
16
|
+
/** Overrides for `palette` (primary, danger, overlay and the other web semantic colours). */
|
|
17
|
+
palette?: Partial<ThemePalette>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ColorValue = string | { light: string; dark: string };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Copies each given key onto `target`. A `{ light, dark }` pair is copied into
|
|
24
|
+
* the existing pair object, so values that hold the pair (`floatingFill`,
|
|
25
|
+
* `floatingHairline`, `floatingSolidFill`) follow it.
|
|
26
|
+
*/
|
|
27
|
+
function assignColors(
|
|
28
|
+
target: { readonly [key: string]: ColorValue },
|
|
29
|
+
source: { readonly [key: string]: ColorValue | undefined },
|
|
30
|
+
) {
|
|
31
|
+
for (const [key, value] of Object.entries(source)) {
|
|
32
|
+
if (value === undefined) continue;
|
|
33
|
+
const current = target[key];
|
|
34
|
+
if (typeof current === "object" && typeof value === "object") Object.assign(current, value);
|
|
35
|
+
else Object.assign(target, { [key]: value });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sets the app's own colours. It writes into the shared `tokens.color` and
|
|
41
|
+
* `palette` objects, which every component reads at render, so call it once at
|
|
42
|
+
* app start, before the first render (for example at the top of the root
|
|
43
|
+
* `_layout.tsx`, outside the component). A call after the first render does not
|
|
44
|
+
* redraw mounted components. Each pair gives both schemes:
|
|
45
|
+
*
|
|
46
|
+
* ```ts
|
|
47
|
+
* configureTheme({
|
|
48
|
+
* color: { background: { light: "#f4f5f6", dark: "#020202" } },
|
|
49
|
+
* palette: { primary: { light: "#09090b", dark: "#fafafa" } },
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* Only the JS colours change. NativeWind classes keep the app's Tailwind theme
|
|
54
|
+
* (`tailwind-preset` reads the package values from source), and constants copied
|
|
55
|
+
* out before the call (`NEUTRAL_500`) keep the package value.
|
|
56
|
+
*/
|
|
57
|
+
export function configureTheme(config: ThemeConfig): void {
|
|
58
|
+
if (config.color) assignColors(tokens.color, config.color);
|
|
59
|
+
if (config.palette) assignColors(palette, config.palette);
|
|
60
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// `@vvedantb/native-ui/markdown`: `Markdown`, chat markdown styled as the web
|
|
2
|
+
// `.markdown` block. Not on the root entry: it imports the optional peer
|
|
3
|
+
// `react-native-marked`, and Metro does not tree-shake.
|
|
4
|
+
export { Markdown, markdownStyles, inlineCodeStyle, MARKDOWN_SCALE, MARKDOWN_LIST } from "../markdown/Markdown";
|
|
5
|
+
export type { MarkdownProps } from "../markdown/Markdown";
|
|
@@ -59,6 +59,8 @@ export type {
|
|
|
59
59
|
TabsContentProps,
|
|
60
60
|
} from "../Tabs";
|
|
61
61
|
export { ListRow } from "../ListRow";
|
|
62
|
+
export { DividedList } from "../DividedList";
|
|
63
|
+
export type { DividedListProps } from "../DividedList";
|
|
62
64
|
export type { ListRowProps } from "../ListRow";
|
|
63
65
|
export { HeaderIconButton } from "../HeaderIconButton";
|
|
64
66
|
export { ThemeToggle } from "../ThemeToggle";
|
|
@@ -100,6 +102,26 @@ export {
|
|
|
100
102
|
} from "../AlertDialog";
|
|
101
103
|
export type { AlertDialogProps, AlertDialogContentProps } from "../AlertDialog";
|
|
102
104
|
export { ConfirmDialog } from "../ConfirmDialog";
|
|
105
|
+
export { BulkActionBar, BULK_BAR_LABEL_MIN_WIDTH } from "../BulkActionBar";
|
|
106
|
+
export type { BulkActionBarProps, BulkBarAction } from "../BulkActionBar";
|
|
107
|
+
export {
|
|
108
|
+
DropdownMenu,
|
|
109
|
+
DropdownMenuTrigger,
|
|
110
|
+
DropdownMenuContent,
|
|
111
|
+
DropdownMenuLabel,
|
|
112
|
+
DropdownMenuItem,
|
|
113
|
+
DropdownMenuRadioItem,
|
|
114
|
+
DropdownMenuSeparator,
|
|
115
|
+
} from "../DropdownMenu";
|
|
116
|
+
export type {
|
|
117
|
+
DropdownMenuProps,
|
|
118
|
+
DropdownMenuTriggerProps,
|
|
119
|
+
DropdownMenuContentProps,
|
|
120
|
+
DropdownMenuAlign,
|
|
121
|
+
DropdownMenuLabelProps,
|
|
122
|
+
DropdownMenuItemProps,
|
|
123
|
+
DropdownMenuRadioItemProps,
|
|
124
|
+
} from "../DropdownMenu";
|
|
103
125
|
export type { ConfirmDialogProps } from "../ConfirmDialog";
|
|
104
126
|
export {
|
|
105
127
|
Sheet,
|
package/src/entries/theme.ts
CHANGED
|
@@ -11,5 +11,7 @@ export {
|
|
|
11
11
|
export type { NativeUiTheme } from "../theme";
|
|
12
12
|
export { palette, withAlpha } from "../palette";
|
|
13
13
|
export type { Palette } from "../palette";
|
|
14
|
+
export { configureTheme } from "../configureTheme";
|
|
15
|
+
export type { ThemeConfig, ThemeColors, ThemePalette } from "../configureTheme";
|
|
14
16
|
export { fontClass } from "../typography";
|
|
15
17
|
export { PINK_50, PINK_100, PINK_600, PINK_700, NEUTRAL_100, NEUTRAL_500, NEUTRAL_900, Colors } from "../colors";
|
|
@@ -7,7 +7,9 @@ import { useState, type ReactNode } from "react";
|
|
|
7
7
|
import { ScrollView, Text, View, type TextProps } from "react-native";
|
|
8
8
|
import {
|
|
9
9
|
IconCopy,
|
|
10
|
+
IconDots,
|
|
10
11
|
IconInbox,
|
|
12
|
+
IconPencil,
|
|
11
13
|
IconPhoto,
|
|
12
14
|
IconRefresh,
|
|
13
15
|
IconTrash,
|
|
@@ -28,6 +30,14 @@ import {
|
|
|
28
30
|
AmbientBackground,
|
|
29
31
|
AppMark,
|
|
30
32
|
AppWordmark,
|
|
33
|
+
BulkActionBar,
|
|
34
|
+
DropdownMenu,
|
|
35
|
+
DropdownMenuContent,
|
|
36
|
+
DropdownMenuItem,
|
|
37
|
+
DropdownMenuLabel,
|
|
38
|
+
DropdownMenuRadioItem,
|
|
39
|
+
DropdownMenuSeparator,
|
|
40
|
+
DropdownMenuTrigger,
|
|
31
41
|
Avatar,
|
|
32
42
|
AvatarFallback,
|
|
33
43
|
Badge,
|
|
@@ -226,6 +236,8 @@ function GalleryScreen() {
|
|
|
226
236
|
const [muted, setMuted] = useState(false);
|
|
227
237
|
const [sort, setSort] = useState<"newest" | "name">("newest");
|
|
228
238
|
const [page, setPage] = useState(0);
|
|
239
|
+
const [menuSort, setMenuSort] = useState<"newest" | "oldest">("newest");
|
|
240
|
+
const [picked, setPicked] = useState(2);
|
|
229
241
|
const scheme = useScheme();
|
|
230
242
|
|
|
231
243
|
return (
|
|
@@ -563,6 +575,50 @@ function GalleryScreen() {
|
|
|
563
575
|
</View>
|
|
564
576
|
</Section>
|
|
565
577
|
|
|
578
|
+
<Section title="DropdownMenu and BulkActionBar">
|
|
579
|
+
<View className="flex-row justify-end">
|
|
580
|
+
<DropdownMenu>
|
|
581
|
+
<DropdownMenuTrigger
|
|
582
|
+
accessibilityLabel="List options"
|
|
583
|
+
hitSlop={6}
|
|
584
|
+
className="h-8 w-8 items-center justify-center"
|
|
585
|
+
>
|
|
586
|
+
<IconDots size={18} color={tokens.color.muted[scheme]} />
|
|
587
|
+
</DropdownMenuTrigger>
|
|
588
|
+
<DropdownMenuContent align="end">
|
|
589
|
+
<DropdownMenuLabel>Sort</DropdownMenuLabel>
|
|
590
|
+
<DropdownMenuRadioItem checked={menuSort === "newest"} onSelect={() => setMenuSort("newest")}>
|
|
591
|
+
Newest first
|
|
592
|
+
</DropdownMenuRadioItem>
|
|
593
|
+
<DropdownMenuRadioItem checked={menuSort === "oldest"} onSelect={() => setMenuSort("oldest")}>
|
|
594
|
+
Oldest first
|
|
595
|
+
</DropdownMenuRadioItem>
|
|
596
|
+
<DropdownMenuSeparator />
|
|
597
|
+
<DropdownMenuItem icon={IconPencil} onSelect={() => toast("Rename")}>
|
|
598
|
+
Rename
|
|
599
|
+
</DropdownMenuItem>
|
|
600
|
+
<DropdownMenuItem icon={IconTrash} destructive onSelect={() => toast.error("Deleted")}>
|
|
601
|
+
Delete
|
|
602
|
+
</DropdownMenuItem>
|
|
603
|
+
</DropdownMenuContent>
|
|
604
|
+
</DropdownMenu>
|
|
605
|
+
</View>
|
|
606
|
+
<View style={{ height: 88 }}>
|
|
607
|
+
<BulkActionBar
|
|
608
|
+
isSelecting
|
|
609
|
+
selectedCount={picked}
|
|
610
|
+
totalCount={5}
|
|
611
|
+
bottomOffset={0}
|
|
612
|
+
onExitSelect={() => setPicked(0)}
|
|
613
|
+
onSelectAll={() => setPicked(5)}
|
|
614
|
+
onClearSelection={() => setPicked(0)}
|
|
615
|
+
primaryActions={[{ key: "copy", label: "Copy items", shortLabel: "Copy", icon: IconCopy, onClick: () => toast("Copied") }]}
|
|
616
|
+
moreActions={[{ key: "rename", label: "Rename", icon: IconPencil, onClick: () => toast("Rename") }]}
|
|
617
|
+
destructiveAction={{ key: "delete", label: "Delete items", shortLabel: "Delete", icon: IconTrash, destructive: true, onClick: () => toast.error("Deleted") }}
|
|
618
|
+
/>
|
|
619
|
+
</View>
|
|
620
|
+
</Section>
|
|
621
|
+
|
|
566
622
|
<Section title="EmptyState">
|
|
567
623
|
<EmptyState title="Nothing yet" description="Items you add show here." compact />
|
|
568
624
|
<EmptyState.Root variant="panel">
|
package/src/floating.ts
CHANGED
|
@@ -45,10 +45,8 @@ export const floatingHairline = tokens.color.hairline;
|
|
|
45
45
|
|
|
46
46
|
export const floatingFill = tokens.color.fill;
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
dark: tokens.color.surface.dark,
|
|
51
|
-
} as const;
|
|
48
|
+
/** The `tokens.color.surface` pair itself, so `configureTheme` reaches it. */
|
|
49
|
+
export const floatingSolidFill = tokens.color.surface;
|
|
52
50
|
|
|
53
51
|
export function tabBarReserve(largerText: boolean, bottomInset: number) {
|
|
54
52
|
const bar = largerText
|
package/src/highlight.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type * as PrismReactRenderer from "prism-react-renderer";
|
|
2
|
+
|
|
3
|
+
export type PrismReactRendererModule = typeof PrismReactRenderer;
|
|
4
|
+
|
|
5
|
+
let prismReactRenderer: PrismReactRendererModule | null | undefined;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `prism-react-renderer`, or `null` when the optional peer is not installed.
|
|
9
|
+
* `CodeBlock` highlights with it. Metro bundles a `require` placed directly in
|
|
10
|
+
* a `try` block as an optional dependency, so a missing peer does not break
|
|
11
|
+
* the bundle.
|
|
12
|
+
*/
|
|
13
|
+
export function loadPrismReactRenderer(): PrismReactRendererModule | null {
|
|
14
|
+
if (prismReactRenderer === undefined) {
|
|
15
|
+
try {
|
|
16
|
+
prismReactRenderer = require("prism-react-renderer");
|
|
17
|
+
} catch {
|
|
18
|
+
prismReactRenderer = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// `?? null`: with the real peer types installed, TypeScript keeps `undefined`
|
|
22
|
+
// in the type after the `try` (the package typecheck only sees a stub).
|
|
23
|
+
return prismReactRenderer ?? null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CodeColorRole =
|
|
27
|
+
| "plain"
|
|
28
|
+
| "comment"
|
|
29
|
+
| "keyword"
|
|
30
|
+
| "string"
|
|
31
|
+
| "constant"
|
|
32
|
+
| "function"
|
|
33
|
+
| "tag"
|
|
34
|
+
| "variable"
|
|
35
|
+
| "inserted"
|
|
36
|
+
| "deleted";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The shiki `github-light` and `github-dark` colours the web `CodeBlock`
|
|
40
|
+
* uses, by role. Plain text is the theme foreground.
|
|
41
|
+
*/
|
|
42
|
+
export const GITHUB_CODE_COLORS: Record<"light" | "dark", Record<CodeColorRole, string>> = {
|
|
43
|
+
light: {
|
|
44
|
+
plain: "#24292e",
|
|
45
|
+
comment: "#6a737d",
|
|
46
|
+
keyword: "#d73a49",
|
|
47
|
+
string: "#032f62",
|
|
48
|
+
constant: "#005cc5",
|
|
49
|
+
function: "#6f42c1",
|
|
50
|
+
tag: "#22863a",
|
|
51
|
+
variable: "#e36209",
|
|
52
|
+
inserted: "#22863a",
|
|
53
|
+
deleted: "#b31d28",
|
|
54
|
+
},
|
|
55
|
+
dark: {
|
|
56
|
+
plain: "#e1e4e8",
|
|
57
|
+
comment: "#6a737d",
|
|
58
|
+
keyword: "#f97583",
|
|
59
|
+
string: "#9ecbff",
|
|
60
|
+
constant: "#79b8ff",
|
|
61
|
+
function: "#b392f0",
|
|
62
|
+
tag: "#85e89d",
|
|
63
|
+
variable: "#ffab70",
|
|
64
|
+
inserted: "#85e89d",
|
|
65
|
+
deleted: "#fdaeb7",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Prism token types to their GitHub role. Types not listed are plain. */
|
|
70
|
+
const PRISM_ROLE: Record<string, CodeColorRole> = {
|
|
71
|
+
comment: "comment",
|
|
72
|
+
prolog: "comment",
|
|
73
|
+
doctype: "comment",
|
|
74
|
+
cdata: "comment",
|
|
75
|
+
keyword: "keyword",
|
|
76
|
+
operator: "keyword",
|
|
77
|
+
atrule: "keyword",
|
|
78
|
+
important: "keyword",
|
|
79
|
+
rule: "keyword",
|
|
80
|
+
string: "string",
|
|
81
|
+
char: "string",
|
|
82
|
+
"attr-value": "string",
|
|
83
|
+
regex: "string",
|
|
84
|
+
url: "string",
|
|
85
|
+
"template-string": "string",
|
|
86
|
+
number: "constant",
|
|
87
|
+
boolean: "constant",
|
|
88
|
+
constant: "constant",
|
|
89
|
+
property: "constant",
|
|
90
|
+
"attr-name": "constant",
|
|
91
|
+
symbol: "constant",
|
|
92
|
+
builtin: "constant",
|
|
93
|
+
entity: "constant",
|
|
94
|
+
function: "function",
|
|
95
|
+
"function-variable": "function",
|
|
96
|
+
"class-name": "function",
|
|
97
|
+
maybe_class_name: "function",
|
|
98
|
+
tag: "tag",
|
|
99
|
+
selector: "tag",
|
|
100
|
+
variable: "variable",
|
|
101
|
+
parameter: "variable",
|
|
102
|
+
inserted: "inserted",
|
|
103
|
+
deleted: "deleted",
|
|
104
|
+
punctuation: "plain",
|
|
105
|
+
plain: "plain",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The colour of a Prism token from its types. Later types win, as in
|
|
110
|
+
* `prism-react-renderer`'s theme lookup: the `<` of a tag (`["tag",
|
|
111
|
+
* "punctuation"]`) is plain.
|
|
112
|
+
*/
|
|
113
|
+
export function codeTokenColor(types: readonly string[], scheme: "light" | "dark"): string {
|
|
114
|
+
let role: CodeColorRole = "plain";
|
|
115
|
+
for (const type of types) role = PRISM_ROLE[type] ?? role;
|
|
116
|
+
return GITHUB_CODE_COLORS[scheme][role];
|
|
117
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ export {
|
|
|
10
10
|
export type { NativeUiTheme } from "./theme";
|
|
11
11
|
export { palette, withAlpha } from "./palette";
|
|
12
12
|
export type { Palette } from "./palette";
|
|
13
|
+
export { configureTheme } from "./configureTheme";
|
|
14
|
+
export type { ThemeConfig, ThemeColors, ThemePalette } from "./configureTheme";
|
|
13
15
|
export { fontClass } from "./typography";
|
|
14
16
|
export {
|
|
15
17
|
FLOATING_INSET,
|
|
@@ -93,6 +95,8 @@ export type {
|
|
|
93
95
|
TabsContentProps,
|
|
94
96
|
} from "./Tabs";
|
|
95
97
|
export { ListRow } from "./ListRow";
|
|
98
|
+
export { DividedList } from "./DividedList";
|
|
99
|
+
export type { DividedListProps } from "./DividedList";
|
|
96
100
|
export type { ListRowProps } from "./ListRow";
|
|
97
101
|
export { HeaderIconButton } from "./HeaderIconButton";
|
|
98
102
|
export { ThemeToggle } from "./ThemeToggle";
|
|
@@ -134,6 +138,26 @@ export {
|
|
|
134
138
|
} from "./AlertDialog";
|
|
135
139
|
export type { AlertDialogProps, AlertDialogContentProps } from "./AlertDialog";
|
|
136
140
|
export { ConfirmDialog } from "./ConfirmDialog";
|
|
141
|
+
export { BulkActionBar, BULK_BAR_LABEL_MIN_WIDTH } from "./BulkActionBar";
|
|
142
|
+
export type { BulkActionBarProps, BulkBarAction } from "./BulkActionBar";
|
|
143
|
+
export {
|
|
144
|
+
DropdownMenu,
|
|
145
|
+
DropdownMenuTrigger,
|
|
146
|
+
DropdownMenuContent,
|
|
147
|
+
DropdownMenuLabel,
|
|
148
|
+
DropdownMenuItem,
|
|
149
|
+
DropdownMenuRadioItem,
|
|
150
|
+
DropdownMenuSeparator,
|
|
151
|
+
} from "./DropdownMenu";
|
|
152
|
+
export type {
|
|
153
|
+
DropdownMenuProps,
|
|
154
|
+
DropdownMenuTriggerProps,
|
|
155
|
+
DropdownMenuContentProps,
|
|
156
|
+
DropdownMenuAlign,
|
|
157
|
+
DropdownMenuLabelProps,
|
|
158
|
+
DropdownMenuItemProps,
|
|
159
|
+
DropdownMenuRadioItemProps,
|
|
160
|
+
} from "./DropdownMenu";
|
|
137
161
|
export type { ConfirmDialogProps } from "./ConfirmDialog";
|
|
138
162
|
export {
|
|
139
163
|
Sheet,
|