@dbx-tools/ui 0.0.0-stage → 0.9.61

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.
@@ -0,0 +1,281 @@
1
+ import { net } from "@dbx-tools/shared-core";
2
+ import { type FormEvent, type ReactNode, useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ import {
5
+ beginPasskeyEnrollment,
6
+ beginPasskeySignIn,
7
+ getAuthStatus,
8
+ requestEmailOtp,
9
+ type PasskeyOperation,
10
+ verifyEmailOtp,
11
+ } from "./auth-client.ts";
12
+ import { BrandIcon, useBrand } from "../../branding/react/index.ts";
13
+ import { Button, Input } from "../../react/index.ts";
14
+
15
+ type Phase = "loading" | "email" | "code" | "enroll" | "authed" | "open";
16
+
17
+ /** Protected content and optional sign-in copy rendered by {@link AuthGate}. */
18
+ export interface AuthGateProps {
19
+ children: ReactNode;
20
+ title?: string;
21
+ description?: string;
22
+ }
23
+
24
+ /** Email OTP gate with conditional passkey sign-in and first-login enrollment. */
25
+ export function AuthGate({ children, title, description }: AuthGateProps): ReactNode {
26
+ const { context: brand } = useBrand();
27
+ const [phase, setPhase] = useState<Phase>("loading");
28
+ const [email, setEmail] = useState("");
29
+ const [code, setCode] = useState("");
30
+ const [busy, setBusy] = useState(false);
31
+ const [notice, setNotice] = useState<string | null>(null);
32
+ const [passkeysEnabled, setPasskeysEnabled] = useState(false);
33
+ const passkeyOperation = useRef<PasskeyOperation | undefined>(undefined);
34
+
35
+ const cancelPasskey = useCallback(() => {
36
+ const operation = passkeyOperation.current;
37
+ passkeyOperation.current = undefined;
38
+ operation?.cancel();
39
+ }, []);
40
+
41
+ useEffect(() => cancelPasskey, [cancelPasskey]);
42
+
43
+ useEffect(() => {
44
+ let cancelled = false;
45
+ void getAuthStatus()
46
+ .then((status) => {
47
+ if (cancelled) return;
48
+ setPasskeysEnabled(status.passkeysEnabled === true);
49
+ setPhase(status.enabled ? (status.authenticated ? "authed" : "email") : "open");
50
+ })
51
+ .catch(() => {
52
+ if (!cancelled) setPhase("email");
53
+ });
54
+ return () => {
55
+ cancelled = true;
56
+ };
57
+ }, []);
58
+
59
+ useEffect(() => {
60
+ if (!passkeysEnabled || phase !== "email") {
61
+ cancelPasskey();
62
+ return;
63
+ }
64
+ if (
65
+ typeof PublicKeyCredential === "undefined" ||
66
+ typeof PublicKeyCredential.isConditionalMediationAvailable !== "function"
67
+ ) {
68
+ return;
69
+ }
70
+ let cancelled = false;
71
+ let ownedOperation: PasskeyOperation | undefined;
72
+ // Conditional mediation cannot reveal whether a credential exists. It lets
73
+ // the browser offer one immediately on the focused `webauthn` input without
74
+ // showing an empty modal to users who have no passkey.
75
+ void PublicKeyCredential.isConditionalMediationAvailable()
76
+ .then(async (available) => {
77
+ if (cancelled) return;
78
+ if (!available) return;
79
+ cancelPasskey();
80
+ const operation = beginPasskeySignIn(true);
81
+ ownedOperation = operation;
82
+ passkeyOperation.current = operation;
83
+ const authenticated = await operation.result;
84
+ if (passkeyOperation.current === operation) {
85
+ passkeyOperation.current = undefined;
86
+ if (!cancelled && authenticated) setPhase("authed");
87
+ }
88
+ })
89
+ .catch(() => undefined);
90
+ return () => {
91
+ cancelled = true;
92
+ ownedOperation?.cancel();
93
+ if (passkeyOperation.current === ownedOperation) passkeyOperation.current = undefined;
94
+ };
95
+ }, [cancelPasskey, passkeysEnabled, phase]);
96
+
97
+ const requestCode = useCallback(
98
+ async (event: FormEvent) => {
99
+ event.preventDefault();
100
+ if (busy) return;
101
+ const addresses = net.parseEmails(email);
102
+ const address = addresses[0];
103
+ if (addresses.length !== 1 || !address || !net.isEmail(address)) {
104
+ setNotice("Enter a valid email address.");
105
+ return;
106
+ }
107
+ cancelPasskey();
108
+ setBusy(true);
109
+ setNotice(null);
110
+ setEmail(address);
111
+ try {
112
+ if (!(await requestEmailOtp(address))) throw new Error("OTP request failed");
113
+ setNotice("If the address is authorized, a verification code is on its way.");
114
+ setPhase("code");
115
+ } catch {
116
+ setNotice("Unable to request a verification code. Try again.");
117
+ } finally {
118
+ setBusy(false);
119
+ }
120
+ },
121
+ [busy, cancelPasskey, email],
122
+ );
123
+
124
+ const verifyCode = useCallback(
125
+ async (event: FormEvent) => {
126
+ event.preventDefault();
127
+ if (!code.trim() || busy) return;
128
+ setBusy(true);
129
+ setNotice(null);
130
+ try {
131
+ if (!(await verifyEmailOtp(email, code.trim(), email.split("@")[0] || "User"))) {
132
+ throw new Error("OTP verification failed");
133
+ }
134
+ setPhase(passkeysEnabled ? "enroll" : "authed");
135
+ } catch {
136
+ setNotice("That verification code is incorrect or has expired.");
137
+ } finally {
138
+ setBusy(false);
139
+ }
140
+ },
141
+ [busy, code, email, passkeysEnabled],
142
+ );
143
+
144
+ const usePasskey = useCallback(async () => {
145
+ cancelPasskey();
146
+ const operation = beginPasskeySignIn();
147
+ passkeyOperation.current = operation;
148
+ setBusy(true);
149
+ setNotice(null);
150
+ try {
151
+ if (!(await operation.result)) throw new Error("Passkey authentication failed");
152
+ if (passkeyOperation.current !== operation) return;
153
+ passkeyOperation.current = undefined;
154
+ setPhase("authed");
155
+ } catch {
156
+ if (passkeyOperation.current === operation) {
157
+ setNotice("Unable to sign in with a passkey. Use email recovery instead.");
158
+ }
159
+ } finally {
160
+ if (passkeyOperation.current === operation) {
161
+ passkeyOperation.current = undefined;
162
+ setBusy(false);
163
+ }
164
+ }
165
+ }, [cancelPasskey]);
166
+
167
+ const enroll = useCallback(async () => {
168
+ cancelPasskey();
169
+ const operation = beginPasskeyEnrollment("Primary passkey");
170
+ passkeyOperation.current = operation;
171
+ setBusy(true);
172
+ setNotice(null);
173
+ try {
174
+ if (!(await operation.result)) throw new Error("Passkey enrollment failed");
175
+ if (passkeyOperation.current !== operation) return;
176
+ passkeyOperation.current = undefined;
177
+ setPhase("authed");
178
+ } catch {
179
+ if (passkeyOperation.current === operation) {
180
+ setNotice("Unable to create a passkey. You can continue with email recovery.");
181
+ }
182
+ } finally {
183
+ if (passkeyOperation.current === operation) {
184
+ passkeyOperation.current = undefined;
185
+ setBusy(false);
186
+ }
187
+ }
188
+ }, [cancelPasskey]);
189
+
190
+ if (phase === "authed" || phase === "open") return <>{children}</>;
191
+ if (phase === "loading") return null;
192
+
193
+ return (
194
+ <div className="flex min-h-screen items-center justify-center bg-background p-6">
195
+ <div className="w-full max-w-sm rounded-lg border border-border bg-card p-6 shadow-sm">
196
+ <div className="mb-4 flex items-center gap-2 text-foreground">
197
+ <BrandIcon className="size-5" alt="" aria-hidden />
198
+ <h1 className="text-lg font-semibold">{title ?? `Sign in to ${brand.name}`}</h1>
199
+ </div>
200
+ <p className="mb-4 text-sm text-muted-foreground">
201
+ {description ??
202
+ (phase === "code"
203
+ ? "Enter the 6-digit verification code sent to your email address."
204
+ : phase === "enroll"
205
+ ? "Create a passkey for faster, phishing-resistant sign-in next time."
206
+ : "Enter your email for a one-time code, or use a passkey.")}
207
+ </p>
208
+
209
+ {phase === "email" ? (
210
+ <form noValidate onSubmit={requestCode} className="space-y-3">
211
+ <Input
212
+ type="email"
213
+ autoComplete="email webauthn"
214
+ autoFocus
215
+ aria-label="Email address"
216
+ placeholder="you@example.com"
217
+ value={email}
218
+ onChange={(event) => setEmail(event.target.value)}
219
+ required
220
+ />
221
+ <Button type="submit" disabled={busy} className="w-full">
222
+ Sign in with email OTP
223
+ </Button>
224
+ {passkeysEnabled ? (
225
+ <Button
226
+ type="button"
227
+ variant="outline"
228
+ disabled={busy}
229
+ className="w-full"
230
+ onClick={usePasskey}
231
+ >
232
+ Sign in with a passkey
233
+ </Button>
234
+ ) : null}
235
+ </form>
236
+ ) : phase === "code" ? (
237
+ <form onSubmit={verifyCode} className="space-y-3">
238
+ <Input
239
+ type="text"
240
+ inputMode="numeric"
241
+ autoComplete="one-time-code"
242
+ aria-label="Verification code"
243
+ placeholder="6-digit verification code"
244
+ maxLength={6}
245
+ value={code}
246
+ onChange={(event) => setCode(event.target.value)}
247
+ required
248
+ />
249
+ <Button type="submit" disabled={busy} className="w-full">
250
+ Continue
251
+ </Button>
252
+ </form>
253
+ ) : (
254
+ <div className="space-y-3">
255
+ <Button type="button" disabled={busy} className="w-full" onClick={enroll}>
256
+ Create a passkey
257
+ </Button>
258
+ <Button
259
+ type="button"
260
+ variant="ghost"
261
+ className="w-full"
262
+ onClick={() => {
263
+ cancelPasskey();
264
+ setBusy(false);
265
+ setPhase("authed");
266
+ }}
267
+ >
268
+ Not now
269
+ </Button>
270
+ </div>
271
+ )}
272
+
273
+ {notice ? (
274
+ <p role="status" aria-live="polite" className="mt-3 text-xs text-muted-foreground">
275
+ {notice}
276
+ </p>
277
+ ) : null}
278
+ </div>
279
+ </div>
280
+ );
281
+ }
@@ -0,0 +1,4 @@
1
+ export { AUTH_BASE_PATH, getAuthStatus, logout } from "./auth-client.ts";
2
+ export type { Passkey } from "./auth-client.ts";
3
+ export { AuthGate, type AuthGateProps } from "./auth-gate.tsx";
4
+ export { PasskeyManager, type PasskeyManagerProps } from "./passkey-manager.tsx";
@@ -0,0 +1,154 @@
1
+ import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ import {
4
+ beginPasskeyEnrollment,
5
+ listPasskeys,
6
+ type PasskeyOperation,
7
+ type Passkey,
8
+ removePasskey,
9
+ renamePasskey,
10
+ } from "./auth-client.ts";
11
+ import { Button, Input } from "../../react/index.ts";
12
+
13
+ /** Styling options for the authenticated passkey management surface. */
14
+ export interface PasskeyManagerProps {
15
+ className?: string;
16
+ }
17
+
18
+ /** Authenticated management surface for a user's Better Auth passkeys. */
19
+ export function PasskeyManager({ className }: PasskeyManagerProps): ReactNode {
20
+ const [passkeys, setPasskeys] = useState<Passkey[]>([]);
21
+ const [names, setNames] = useState<Record<string, string>>({});
22
+ const [busy, setBusy] = useState(false);
23
+ const [loaded, setLoaded] = useState(false);
24
+ const [notice, setNotice] = useState<string | null>(null);
25
+ const enrollment = useRef<PasskeyOperation | undefined>(undefined);
26
+
27
+ const cancelEnrollment = useCallback(() => {
28
+ const operation = enrollment.current;
29
+ enrollment.current = undefined;
30
+ operation?.cancel();
31
+ }, []);
32
+
33
+ useEffect(() => cancelEnrollment, [cancelEnrollment]);
34
+
35
+ const refresh = useCallback(async () => {
36
+ const values = await listPasskeys();
37
+ setPasskeys(values);
38
+ setNames(Object.fromEntries(values.map((passkey) => [passkey.id, passkey.name ?? "Passkey"])));
39
+ setLoaded(true);
40
+ setNotice(null);
41
+ }, []);
42
+
43
+ useEffect(() => {
44
+ void refresh().catch(() => setNotice("Unable to load passkeys."));
45
+ }, [refresh]);
46
+
47
+ const add = useCallback(async () => {
48
+ cancelEnrollment();
49
+ const operation = beginPasskeyEnrollment();
50
+ enrollment.current = operation;
51
+ setBusy(true);
52
+ setNotice(null);
53
+ try {
54
+ if (!(await operation.result)) throw new Error("Passkey enrollment failed");
55
+ if (enrollment.current !== operation) return;
56
+ await refresh();
57
+ } catch {
58
+ if (enrollment.current === operation) setNotice("Unable to add a passkey.");
59
+ } finally {
60
+ if (enrollment.current === operation) {
61
+ enrollment.current = undefined;
62
+ setBusy(false);
63
+ }
64
+ }
65
+ }, [cancelEnrollment, refresh]);
66
+
67
+ const rename = useCallback(
68
+ async (id: string) => {
69
+ const name = names[id]?.trim();
70
+ if (!name) return;
71
+ setBusy(true);
72
+ try {
73
+ if (!(await renamePasskey(id, name))) throw new Error("Passkey rename failed");
74
+ await refresh();
75
+ } catch {
76
+ setNotice("Unable to rename the passkey.");
77
+ } finally {
78
+ setBusy(false);
79
+ }
80
+ },
81
+ [names, refresh],
82
+ );
83
+
84
+ const remove = useCallback(
85
+ async (id: string) => {
86
+ setBusy(true);
87
+ try {
88
+ if (!(await removePasskey(id))) throw new Error("Passkey removal failed");
89
+ await refresh();
90
+ } catch {
91
+ setNotice("Unable to remove the passkey.");
92
+ } finally {
93
+ setBusy(false);
94
+ }
95
+ },
96
+ [refresh],
97
+ );
98
+
99
+ return (
100
+ <section className={className} aria-label="Passkeys">
101
+ <div className="mb-3 flex items-center justify-between gap-3">
102
+ <div>
103
+ <h2 className="font-medium text-foreground">Passkeys</h2>
104
+ <p className="text-sm text-muted-foreground">
105
+ Add more than one device so email remains recovery, not your daily sign-in.
106
+ </p>
107
+ </div>
108
+ <Button type="button" onClick={add} disabled={busy}>
109
+ Add passkey
110
+ </Button>
111
+ </div>
112
+ <div className="space-y-2">
113
+ {passkeys.map((passkey) => (
114
+ <div key={passkey.id} className="flex items-center gap-2 rounded-md border p-2">
115
+ <Input
116
+ aria-label="Passkey name"
117
+ value={names[passkey.id] ?? ""}
118
+ onChange={(event) =>
119
+ setNames((current) => ({ ...current, [passkey.id]: event.target.value }))
120
+ }
121
+ />
122
+ <Button
123
+ type="button"
124
+ variant="outline"
125
+ disabled={busy}
126
+ onClick={() => rename(passkey.id)}
127
+ >
128
+ Save
129
+ </Button>
130
+ <Button
131
+ type="button"
132
+ variant="ghost"
133
+ disabled={busy}
134
+ onClick={() => remove(passkey.id)}
135
+ >
136
+ Remove
137
+ </Button>
138
+ </div>
139
+ ))}
140
+ {loaded && passkeys.length === 0 ? (
141
+ <p className="text-sm text-muted-foreground">No passkeys enrolled.</p>
142
+ ) : null}
143
+ {!loaded && !notice ? (
144
+ <p className="text-sm text-muted-foreground">Loading passkeys.</p>
145
+ ) : null}
146
+ </div>
147
+ {notice ? (
148
+ <p role="status" className="mt-2 text-sm text-muted-foreground">
149
+ {notice}
150
+ </p>
151
+ ) : null}
152
+ </section>
153
+ );
154
+ }
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Brand -> AppKit token bridge.
3
+ *
4
+ * Remaps the AppKit / shadcn semantic tokens the dbx-tools UI components
5
+ * actually style off (`--primary`, `--ring`, `--accent`, `--sidebar-*`,
6
+ * `--font-*`) onto the portable `--brand-color-*` / `--brand-font-*` custom
7
+ * properties that `applyBrandContext()` writes. This is the "tag selector":
8
+ * scoped to `:root[data-brand]`, it is completely INERT until a brand is
9
+ * applied to the document (which sets the `data-brand` attribute), so
10
+ * importing it never disturbs default AppKit.
11
+ *
12
+ * Import once via `@dbx-tools/ui/styles.css` (the shared UI base every
13
+ * feature UI package pulls in), so the bridge travels with every dbx-tools UI
14
+ * component automatically.
15
+ *
16
+ * Scope note: `:root[data-brand]` has higher specificity (0,2,0) than AppKit's
17
+ * `:root` and `.dark` (both 0,1,0), so these win regardless of source order.
18
+ *
19
+ * Deliberately identity-only: it does NOT remap neutrals (`--background`,
20
+ * `--foreground`, `--card`, `--muted`, `--border`). The brand context carries a
21
+ * single light palette, so remapping neutrals would break dark mode; leaving
22
+ * them to AppKit keeps light AND dark themes working while the brand's identity
23
+ * colors + fonts come through.
24
+ */
25
+ :root[data-brand] {
26
+ --primary: var(--brand-color-primary);
27
+ --primary-foreground: var(--brand-color-background);
28
+ --ring: var(--brand-color-primary);
29
+ /*
30
+ * `--accent` is a SURFACE, not an identity color. In AppKit / shadcn it is the
31
+ * subtle hover + selected background (light theme `oklch(0.967 0.001 286)`,
32
+ * i.e. near-white) paired with a near-black `--accent-foreground`. Assigning
33
+ * the brand accent to it at full strength therefore put dark text on saturated
34
+ * `#00A972` in every hover and every selected row - the selected conversation
35
+ * in `ThreadSidebar`, which styles off `bg-accent text-accent-foreground`, was
36
+ * the worst of it.
37
+ *
38
+ * Menus, selected rows, and tabs are navigation, not success or identity
39
+ * signals. Tint them with the cooler primary-hover color rather than the
40
+ * identity accent so the default brand produces a quiet blue wash instead of
41
+ * coral/red or success green. Mixing into the CURRENT `--background` follows
42
+ * the active theme on its own - pale in light mode and deep in dark mode -
43
+ * while AppKit's per-theme `--accent-foreground` remains readable. That is
44
+ * also why `--accent-foreground` is NOT set here: pinning it would break
45
+ * whichever theme it was not chosen for.
46
+ */
47
+ --accent: color-mix(in oklab, var(--brand-color-primary-hover) 12%, var(--background));
48
+ --sidebar-primary: var(--brand-color-primary);
49
+ --sidebar-primary-foreground: var(--brand-color-background);
50
+ --sidebar-ring: var(--brand-color-primary);
51
+ --font-sans: var(--brand-font-sans);
52
+ --font-mono: var(--brand-font-mono);
53
+ }
@@ -0,0 +1,70 @@
1
+ import { brandUtils } from "@dbx-tools/shared-core";
2
+ import { dbxToolsAssetDataUrls } from "./generated/assets.ts";
3
+
4
+ const builtInAssets = new Map<string, string>([
5
+ [brandUtils.DEFAULT_BRAND_ASSETS.icon.light, dbxToolsAssetDataUrls.iconLight],
6
+ [brandUtils.DEFAULT_BRAND_ASSETS.icon.dark, dbxToolsAssetDataUrls.iconDark],
7
+ [brandUtils.DEFAULT_BRAND_ASSETS.logo.light, dbxToolsAssetDataUrls.logoLight],
8
+ [brandUtils.DEFAULT_BRAND_ASSETS.logo.dark, dbxToolsAssetDataUrls.logoDark],
9
+ [brandUtils.DEFAULT_BRAND_ASSETS.favicon, dbxToolsAssetDataUrls.iconLight],
10
+ ]);
11
+
12
+ /** Resolve a brand asset identifier to a browser-loadable URL. */
13
+ export type BrandAssetResolver = (source: string) => string;
14
+
15
+ /** Resolve built-in package asset ids to portable data URLs. */
16
+ export const resolveBrandAsset: BrandAssetResolver = (source) =>
17
+ builtInAssets.get(source) ?? source;
18
+
19
+ /** Convert a validated context to CSS custom properties. */
20
+ export function brandCssVariables(context: brandUtils.BrandContext): Record<string, string> {
21
+ return {
22
+ "--brand-color-primary": context.colors.primary,
23
+ "--brand-color-primary-hover": context.colors.primaryHover,
24
+ "--brand-color-accent": context.colors.accent,
25
+ "--brand-color-foreground": context.colors.foreground,
26
+ "--brand-color-background": context.colors.background,
27
+ "--brand-color-surface": context.colors.surface,
28
+ "--brand-color-muted": context.colors.muted,
29
+ "--brand-color-border": context.colors.border,
30
+ "--brand-font-sans": context.typography.sans,
31
+ "--brand-font-mono": context.typography.mono,
32
+ };
33
+ }
34
+
35
+ /** Document, root element, asset, title, and favicon behavior for applying a brand. */
36
+ export interface ApplyBrandContextOptions {
37
+ root?: HTMLElement;
38
+ document?: Document;
39
+ resolveAsset?: BrandAssetResolver;
40
+ updateTitle?: boolean;
41
+ updateFavicon?: boolean;
42
+ }
43
+
44
+ /** Apply tokens and optional page metadata to a browser document. */
45
+ export function applyBrandContext(
46
+ context: brandUtils.BrandContext,
47
+ options: ApplyBrandContextOptions = {},
48
+ ): void {
49
+ const documentRef = options.document ?? globalThis.document;
50
+ const root = options.root ?? documentRef?.documentElement;
51
+ for (const [name, value] of Object.entries(brandCssVariables(context))) {
52
+ root?.style.setProperty(name, value);
53
+ }
54
+ // Detection flag for the `brand-bridge.css` tag selector: setting
55
+ // `data-brand` activates the `:root[data-brand]` token remap. Until a
56
+ // brand is applied the bridge is inert, so default AppKit is untouched.
57
+ root?.setAttribute("data-brand", context.name);
58
+
59
+ if (documentRef && options.updateTitle !== false) documentRef.title = context.name;
60
+ if (!documentRef || options.updateFavicon === false) return;
61
+
62
+ const resolveAsset = options.resolveAsset ?? resolveBrandAsset;
63
+ let favicon = documentRef.querySelector<HTMLLinkElement>('link[rel="icon"]');
64
+ if (!favicon) {
65
+ favicon = documentRef.createElement("link");
66
+ favicon.rel = "icon";
67
+ documentRef.head.append(favicon);
68
+ }
69
+ favicon.href = resolveAsset(context.assets.favicon);
70
+ }
@@ -0,0 +1,18 @@
1
+ // GENERATED by branding/generate-package-assets.mjs - DO NOT EDIT.
2
+ // Regenerated from branding/assets.
3
+ // Hand edits are overwritten during synthesis; this file is read-only.
4
+ /* eslint-disable prettier/prettier */
5
+
6
+ export const dbxToolsAssetSvg = {
7
+ iconLight: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" role=\"img\" aria-labelledby=\"title description\">\n <title id=\"title\">dbx tools icon</title>\n <desc id=\"description\">Pixel tool mark in Databricks lava red on a light surface.</desc>\n <rect width=\"64\" height=\"64\" rx=\"12\" fill=\"#F9F7F4\"/>\n <g shape-rendering=\"crispEdges\">\n <path d=\"M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z\" fill=\"#FF3621\"/>\n <path d=\"M4 28h8v8H4zm52 0h4v8h-4z\" fill=\"#98102A\"/>\n <path d=\"M28 28h8v8h-8z\" fill=\"#FF5F46\"/>\n </g>\n</svg>\n",
8
+ iconDark: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\" role=\"img\" aria-labelledby=\"title description\">\n <title id=\"title\">dbx tools icon</title>\n <desc id=\"description\">Pixel tool mark in Databricks lava red on a dark surface.</desc>\n <rect width=\"64\" height=\"64\" rx=\"12\" fill=\"#1B3139\"/>\n <g shape-rendering=\"crispEdges\">\n <path d=\"M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z\" fill=\"#FF3621\"/>\n <path d=\"M4 28h8v8H4zm52 0h4v8h-4z\" fill=\"#FF5F46\"/>\n <path d=\"M28 28h8v8h-8z\" fill=\"#FF8974\"/>\n </g>\n</svg>\n",
9
+ logoLight: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 72\" role=\"img\" aria-labelledby=\"title description\">\n <title id=\"title\">dbx tools</title>\n <desc id=\"description\">dbx tools logo for light surfaces.</desc>\n <g transform=\"translate(4 4)\">\n <rect width=\"64\" height=\"64\" rx=\"12\" fill=\"#F9F7F4\"/>\n <g shape-rendering=\"crispEdges\">\n <path d=\"M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z\" fill=\"#FF3621\"/>\n <path d=\"M4 28h8v8H4zm52 0h4v8h-4z\" fill=\"#98102A\"/>\n <path d=\"M28 28h8v8h-8z\" fill=\"#FF5F46\"/>\n </g>\n </g>\n <text x=\"84\" y=\"47\" font-family=\"'DM Sans', ui-sans-serif, system-ui, sans-serif\" font-size=\"32\" font-weight=\"700\" letter-spacing=\"-0.5\" fill=\"#1B3139\">dbx <tspan fill=\"#FF3621\">tools</tspan></text>\n</svg>\n",
10
+ logoDark: "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 72\" role=\"img\" aria-labelledby=\"title description\">\n <title id=\"title\">dbx tools</title>\n <desc id=\"description\">dbx tools logo for dark surfaces.</desc>\n <g transform=\"translate(4 4)\">\n <rect width=\"64\" height=\"64\" rx=\"12\" fill=\"#1B3139\"/>\n <g shape-rendering=\"crispEdges\">\n <path d=\"M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z\" fill=\"#FF3621\"/>\n <path d=\"M4 28h8v8H4zm52 0h4v8h-4z\" fill=\"#FF5F46\"/>\n <path d=\"M28 28h8v8h-8z\" fill=\"#FF8974\"/>\n </g>\n </g>\n <text x=\"84\" y=\"47\" font-family=\"'DM Sans', ui-sans-serif, system-ui, sans-serif\" font-size=\"32\" font-weight=\"700\" letter-spacing=\"-0.5\" fill=\"#F9F7F4\">dbx <tspan fill=\"#FF5F46\">tools</tspan></text>\n</svg>\n",
11
+ } as const;
12
+
13
+ export const dbxToolsAssetDataUrls = {
14
+ iconLight: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InRpdGxlIGRlc2NyaXB0aW9uIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5kYnggdG9vbHMgaWNvbjwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2NyaXB0aW9uIj5QaXhlbCB0b29sIG1hcmsgaW4gRGF0YWJyaWNrcyBsYXZhIHJlZCBvbiBhIGxpZ2h0IHN1cmZhY2UuPC9kZXNjPgogIDxyZWN0IHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgcng9IjEyIiBmaWxsPSIjRjlGN0Y0Ii8+CiAgPGcgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KICAgIDxwYXRoIGQ9Ik00IDI4aDh2OEg0em04IDBoOHYxNmgtOHptOC04aDh2MTZoLTh6bTggOGg4djhoLTh6bTgtOGg4djE2aC04em04IDhoOHYxNmgtOHptOCAwaDR2OGgtNHoiIGZpbGw9IiNGRjM2MjEiLz4KICAgIDxwYXRoIGQ9Ik00IDI4aDh2OEg0em01MiAwaDR2OGgtNHoiIGZpbGw9IiM5ODEwMkEiLz4KICAgIDxwYXRoIGQ9Ik0yOCAyOGg4djhoLTh6IiBmaWxsPSIjRkY1RjQ2Ii8+CiAgPC9nPgo8L3N2Zz4K",
15
+ iconDark: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InRpdGxlIGRlc2NyaXB0aW9uIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5kYnggdG9vbHMgaWNvbjwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2NyaXB0aW9uIj5QaXhlbCB0b29sIG1hcmsgaW4gRGF0YWJyaWNrcyBsYXZhIHJlZCBvbiBhIGRhcmsgc3VyZmFjZS48L2Rlc2M+CiAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iMTIiIGZpbGw9IiMxQjMxMzkiLz4KICA8ZyBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiPgogICAgPHBhdGggZD0iTTQgMjhoOHY4SDR6bTggMGg4djE2aC04em04LThoOHYxNmgtOHptOCA4aDh2OGgtOHptOC04aDh2MTZoLTh6bTggOGg4djE2aC04em04IDBoNHY4aC00eiIgZmlsbD0iI0ZGMzYyMSIvPgogICAgPHBhdGggZD0iTTQgMjhoOHY4SDR6bTUyIDBoNHY4aC00eiIgZmlsbD0iI0ZGNUY0NiIvPgogICAgPHBhdGggZD0iTTI4IDI4aDh2OGgtOHoiIGZpbGw9IiNGRjg5NzQiLz4KICA8L2c+Cjwvc3ZnPgo=",
16
+ logoLight: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMjAgNzIiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjcmlwdGlvbiI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+ZGJ4IHRvb2xzPC90aXRsZT4KICA8ZGVzYyBpZD0iZGVzY3JpcHRpb24iPmRieCB0b29scyBsb2dvIGZvciBsaWdodCBzdXJmYWNlcy48L2Rlc2M+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNCA0KSI+CiAgICA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxMiIgZmlsbD0iI0Y5RjdGNCIvPgogICAgPGcgc2hhcGUtcmVuZGVyaW5nPSJjcmlzcEVkZ2VzIj4KICAgICAgPHBhdGggZD0iTTQgMjhoOHY4SDR6bTggMGg4djE2aC04em04LThoOHYxNmgtOHptOCA4aDh2OGgtOHptOC04aDh2MTZoLTh6bTggOGg4djE2aC04em04IDBoNHY4aC00eiIgZmlsbD0iI0ZGMzYyMSIvPgogICAgICA8cGF0aCBkPSJNNCAyOGg4djhINHptNTIgMGg0djhoLTR6IiBmaWxsPSIjOTgxMDJBIi8+CiAgICAgIDxwYXRoIGQ9Ik0yOCAyOGg4djhoLTh6IiBmaWxsPSIjRkY1RjQ2Ii8+CiAgICA8L2c+CiAgPC9nPgogIDx0ZXh0IHg9Ijg0IiB5PSI0NyIgZm9udC1mYW1pbHk9IidETSBTYW5zJywgdWktc2Fucy1zZXJpZiwgc3lzdGVtLXVpLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjMyIiBmb250LXdlaWdodD0iNzAwIiBsZXR0ZXItc3BhY2luZz0iLTAuNSIgZmlsbD0iIzFCMzEzOSI+ZGJ4IDx0c3BhbiBmaWxsPSIjRkYzNjIxIj50b29sczwvdHNwYW4+PC90ZXh0Pgo8L3N2Zz4K",
17
+ logoDark: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMjAgNzIiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjcmlwdGlvbiI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+ZGJ4IHRvb2xzPC90aXRsZT4KICA8ZGVzYyBpZD0iZGVzY3JpcHRpb24iPmRieCB0b29scyBsb2dvIGZvciBkYXJrIHN1cmZhY2VzLjwvZGVzYz4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0IDQpIj4KICAgIDxyZWN0IHdpZHRoPSI2NCIgaGVpZ2h0PSI2NCIgcng9IjEyIiBmaWxsPSIjMUIzMTM5Ii8+CiAgICA8ZyBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiPgogICAgICA8cGF0aCBkPSJNNCAyOGg4djhINHptOCAwaDh2MTZoLTh6bTgtOGg4djE2aC04em04IDhoOHY4aC04em04LThoOHYxNmgtOHptOCA4aDh2MTZoLTh6bTggMGg0djhoLTR6IiBmaWxsPSIjRkYzNjIxIi8+CiAgICAgIDxwYXRoIGQ9Ik00IDI4aDh2OEg0em01MiAwaDR2OGgtNHoiIGZpbGw9IiNGRjVGNDYiLz4KICAgICAgPHBhdGggZD0iTTI4IDI4aDh2OGgtOHoiIGZpbGw9IiNGRjg5NzQiLz4KICAgIDwvZz4KICA8L2c+CiAgPHRleHQgeD0iODQiIHk9IjQ3IiBmb250LWZhbWlseT0iJ0RNIFNhbnMnLCB1aS1zYW5zLXNlcmlmLCBzeXN0ZW0tdWksIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMzIiIGZvbnQtd2VpZ2h0PSI3MDAiIGxldHRlci1zcGFjaW5nPSItMC41IiBmaWxsPSIjRjlGN0Y0Ij5kYnggPHRzcGFuIGZpbGw9IiNGRjVGNDYiPnRvb2xzPC90c3Bhbj48L3RleHQ+Cjwvc3ZnPgo=",
18
+ } as const;
@@ -0,0 +1,11 @@
1
+ <!-- GENERATED by branding/generate-package-assets.mjs - DO NOT EDIT. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title description">
3
+ <title id="title">dbx tools icon</title>
4
+ <desc id="description">Pixel tool mark in Databricks lava red on a dark surface.</desc>
5
+ <rect width="64" height="64" rx="12" fill="#1B3139"/>
6
+ <g shape-rendering="crispEdges">
7
+ <path d="M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z" fill="#FF3621"/>
8
+ <path d="M4 28h8v8H4zm52 0h4v8h-4z" fill="#FF5F46"/>
9
+ <path d="M28 28h8v8h-8z" fill="#FF8974"/>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,11 @@
1
+ <!-- GENERATED by branding/generate-package-assets.mjs - DO NOT EDIT. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title description">
3
+ <title id="title">dbx tools icon</title>
4
+ <desc id="description">Pixel tool mark in Databricks lava red on a light surface.</desc>
5
+ <rect width="64" height="64" rx="12" fill="#F9F7F4"/>
6
+ <g shape-rendering="crispEdges">
7
+ <path d="M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z" fill="#FF3621"/>
8
+ <path d="M4 28h8v8H4zm52 0h4v8h-4z" fill="#98102A"/>
9
+ <path d="M28 28h8v8h-8z" fill="#FF5F46"/>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,14 @@
1
+ <!-- GENERATED by branding/generate-package-assets.mjs - DO NOT EDIT. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 72" role="img" aria-labelledby="title description">
3
+ <title id="title">dbx tools</title>
4
+ <desc id="description">dbx tools logo for dark surfaces.</desc>
5
+ <g transform="translate(4 4)">
6
+ <rect width="64" height="64" rx="12" fill="#1B3139"/>
7
+ <g shape-rendering="crispEdges">
8
+ <path d="M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z" fill="#FF3621"/>
9
+ <path d="M4 28h8v8H4zm52 0h4v8h-4z" fill="#FF5F46"/>
10
+ <path d="M28 28h8v8h-8z" fill="#FF8974"/>
11
+ </g>
12
+ </g>
13
+ <text x="84" y="47" font-family="'DM Sans', ui-sans-serif, system-ui, sans-serif" font-size="32" font-weight="700" letter-spacing="-0.5" fill="#F9F7F4">dbx <tspan fill="#FF5F46">tools</tspan></text>
14
+ </svg>
@@ -0,0 +1,14 @@
1
+ <!-- GENERATED by branding/generate-package-assets.mjs - DO NOT EDIT. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 72" role="img" aria-labelledby="title description">
3
+ <title id="title">dbx tools</title>
4
+ <desc id="description">dbx tools logo for light surfaces.</desc>
5
+ <g transform="translate(4 4)">
6
+ <rect width="64" height="64" rx="12" fill="#F9F7F4"/>
7
+ <g shape-rendering="crispEdges">
8
+ <path d="M4 28h8v8H4zm8 0h8v16h-8zm8-8h8v16h-8zm8 8h8v8h-8zm8-8h8v16h-8zm8 8h8v16h-8zm8 0h4v8h-4z" fill="#FF3621"/>
9
+ <path d="M4 28h8v8H4zm52 0h4v8h-4z" fill="#98102A"/>
10
+ <path d="M28 28h8v8h-8z" fill="#FF5F46"/>
11
+ </g>
12
+ </g>
13
+ <text x="84" y="47" font-family="'DM Sans', ui-sans-serif, system-ui, sans-serif" font-size="32" font-weight="700" letter-spacing="-0.5" fill="#1B3139">dbx <tspan fill="#FF3621">tools</tspan></text>
14
+ </svg>