@xzibit/ui 0.1.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.
@@ -0,0 +1,153 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ interface TopBarProps {
4
+ /** App display name shown in the wordmark slot (e.g. "ERP Overview"). */
5
+ appName: string;
6
+ /** Optional click target for the app wordmark. Defaults to "/" (app home). */
7
+ appHomeUrl?: string;
8
+ /** Build SHA — typically `process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 7)`. */
9
+ buildSha?: string;
10
+ /**
11
+ * Last-updated timestamp string. Pre-format on the consumer side
12
+ * (e.g. "22 May 2026, 5:03 pm AEST"). Brisbane time recommended per
13
+ * portfolio convention.
14
+ */
15
+ buildTimestamp?: string;
16
+ /** Launcher URL override — defaults to https://xzibit-apps.vercel.app. */
17
+ launcherUrl?: string;
18
+ /** Override the /api/me/apps endpoint for the dropdown. */
19
+ appsEndpoint?: string;
20
+ }
21
+ /**
22
+ * Universal 44px fixed top bar across every Xzibit App.
23
+ *
24
+ * Per DESIGN-STANDARD v2.3 + v2.3.1 §Top Bar:
25
+ * - Absorbs back-to-launcher anchor + app wordmark + build badge
26
+ * - Adds Apps dropdown (dynamic data, sectioned + within-section alphabetical)
27
+ * - Sits above the left rail (Pattern A apps) at z-index 100
28
+ * - Page content offsets `margin-top: 44px`
29
+ *
30
+ * Reference impl: xzibit-apps/erp-overview SHA 6f1a5b5+
31
+ */
32
+ declare function TopBar({ appName, appHomeUrl, buildSha, buildTimestamp, launcherUrl, appsEndpoint, }: TopBarProps): react_jsx_runtime.JSX.Element;
33
+
34
+ interface BackToLauncherProps {
35
+ /** Launcher URL — defaults to the production Xzibit Apps launcher. */
36
+ launcherUrl?: string;
37
+ }
38
+ /**
39
+ * Back-to-launcher anchor for the TopBar's left cluster.
40
+ *
41
+ * Renders `[chevron-left] [Xzibit X logo] [Xzibit Apps text]` inside a single
42
+ * `<a>` element so the whole cluster is one click target with one navigation
43
+ * destination. Cross-deployment navigation — uses native anchor, NOT Next.js
44
+ * Link (cross-domain).
45
+ *
46
+ * Per DESIGN-STANDARD v2.3.1:
47
+ * - Text "Xzibit Apps" at 15px / 500 / 85% white (full white on hover)
48
+ * - Chevron 12×12 at 70% white (full white on hover)
49
+ * - X logo 28×28 unchanged on hover (stays native white-on-black stamp)
50
+ */
51
+ declare function BackToLauncher({ launcherUrl, }: BackToLauncherProps): react_jsx_runtime.JSX.Element;
52
+
53
+ interface AppsDropdownProps {
54
+ /** Override the endpoint URL. Default: '/api/me/apps' via useApps(). */
55
+ endpoint?: string;
56
+ }
57
+ /**
58
+ * Apps dropdown for the TopBar — sectioned + within-section alphabetical.
59
+ *
60
+ * Per DESIGN-STANDARD v2.3.1 §Top Bar §Apps dropdown panel:
61
+ * - Groups apps by `section` field
62
+ * - Section order from `section_order` (matches launcher curation)
63
+ * - Within each section: alphabetical by `name` ASC, case-insensitive
64
+ * - Section heading: 11px / 500 / muted / uppercase / +0.06em letter-spacing
65
+ * - Section divider: 1px var(--border), 8px margin top/bottom — NOT above
66
+ * the first section, NOT below the last
67
+ * - Skeleton during fetch; empty-state copy if user has no other apps
68
+ *
69
+ * Accessibility: opens on click; closes on Esc; closes on outside-click;
70
+ * focus returns to trigger on close. Items are role="menuitem".
71
+ */
72
+ declare function AppsDropdown({ endpoint }?: AppsDropdownProps): react_jsx_runtime.JSX.Element;
73
+
74
+ interface XzibitMarkProps {
75
+ /** Size in pixels (square). Default 28 to fit inside the 44px TopBar. */
76
+ size?: number;
77
+ /** Optional className for additional styling. */
78
+ className?: string;
79
+ /** If provided, sets the accessible name. Omit for decorative usage (defaults to aria-hidden). */
80
+ ariaLabel?: string;
81
+ }
82
+ /**
83
+ * Xzibit X brand mark, rendered as inline SVG for crisp display at any pixel density.
84
+ *
85
+ * Native black background — the "stamp" treatment that gives the mark visual weight
86
+ * against the var(--xz-charcoal) TopBar background. The mark is geometric / angular,
87
+ * approximating the architectural character of the brand asset.
88
+ *
89
+ * Default usage is decorative (aria-hidden). Provide `ariaLabel` for cases where
90
+ * the mark is the sole semantic content of an interactive element.
91
+ */
92
+ declare function XzibitMark({ size, className, ariaLabel }: XzibitMarkProps): react_jsx_runtime.JSX.Element;
93
+
94
+ /**
95
+ * Shared types for @xzibit/ui.
96
+ */
97
+ /**
98
+ * An app in the Xzibit Apps portfolio.
99
+ *
100
+ * Shape matches the response from each app's `/api/me/apps` endpoint,
101
+ * which queries `public.apps` JOIN `public.role_app_permissions` and returns
102
+ * the apps the authenticated user has access to per their role.
103
+ */
104
+ interface App {
105
+ /** Display name, e.g. "Capacity Planner". */
106
+ name: string;
107
+ /** Full URL including protocol, e.g. "https://xzibit-capacity-planner.vercel.app". */
108
+ url: string;
109
+ /** Optional one-line description rendered as secondary text in the dropdown. */
110
+ description?: string;
111
+ /** Section grouping label, e.g. "Strategic", "Calculators". Null = no section. */
112
+ section?: string | null;
113
+ /** Sort order for the section itself (matches launcher curation). */
114
+ section_order?: number;
115
+ }
116
+ /**
117
+ * Response shape from `/api/me/apps`.
118
+ *
119
+ * Per CODING-STANDARDS §6.4 — successful responses are wrapped in a `data`
120
+ * or domain-specific key (in this case `apps`).
121
+ */
122
+ interface AppsResponse {
123
+ apps?: App[];
124
+ error?: string;
125
+ }
126
+
127
+ interface UseAppsResult {
128
+ apps: App[];
129
+ loading: boolean;
130
+ error: string | null;
131
+ refetch: () => Promise<void>;
132
+ }
133
+ interface UseAppsOptions {
134
+ /** Override the endpoint URL. Default: '/api/me/apps' (same-origin per-app endpoint). */
135
+ endpoint?: string;
136
+ /** If true, defer the initial fetch until refetch() is called explicitly. Default false (fetch on mount). */
137
+ lazy?: boolean;
138
+ }
139
+ /**
140
+ * Fetch the list of Xzibit Apps the current user has access to.
141
+ *
142
+ * Each consuming app exposes its own `/api/me/apps` endpoint (same-origin)
143
+ * that queries shared `public.apps` + `public.role_app_permissions` and
144
+ * returns the filtered list. This hook wraps the fetch with loading + error
145
+ * state and an exposed `refetch()`.
146
+ *
147
+ * Per DESIGN-STANDARD v2.3 §Top Bar — the dropdown MUST be dynamic for
148
+ * production (no hardcoded lists). Adding a new portfolio app then becomes
149
+ * one INSERT in `public.apps` + role grants — zero per-app code changes.
150
+ */
151
+ declare function useApps(options?: UseAppsOptions): UseAppsResult;
152
+
153
+ export { type App, AppsDropdown, type AppsDropdownProps, type AppsResponse, BackToLauncher, type BackToLauncherProps, TopBar, type TopBarProps, type UseAppsOptions, type UseAppsResult, XzibitMark, type XzibitMarkProps, useApps };
package/dist/index.js ADDED
@@ -0,0 +1,524 @@
1
+ // src/BackToLauncher.tsx
2
+ import { useState } from "react";
3
+
4
+ // src/XzibitMark.tsx
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ function XzibitMark({ size = 28, className, ariaLabel }) {
7
+ return /* @__PURE__ */ jsxs(
8
+ "svg",
9
+ {
10
+ width: size,
11
+ height: size,
12
+ viewBox: "0 0 100 100",
13
+ xmlns: "http://www.w3.org/2000/svg",
14
+ role: ariaLabel ? "img" : "presentation",
15
+ "aria-label": ariaLabel,
16
+ "aria-hidden": ariaLabel ? void 0 : true,
17
+ className,
18
+ children: [
19
+ /* @__PURE__ */ jsx("rect", { x: 0, y: 0, width: 100, height: 100, fill: "#000000" }),
20
+ /* @__PURE__ */ jsxs(
21
+ "g",
22
+ {
23
+ stroke: "#FFFFFF",
24
+ strokeWidth: 9,
25
+ fill: "none",
26
+ strokeLinecap: "square",
27
+ strokeLinejoin: "miter",
28
+ children: [
29
+ /* @__PURE__ */ jsx("path", { d: "M 22 22 L 30 22 L 30 32 L 50 50" }),
30
+ /* @__PURE__ */ jsx("path", { d: "M 78 22 L 70 22 L 70 32 L 50 50" }),
31
+ /* @__PURE__ */ jsx("path", { d: "M 22 78 L 30 78 L 30 68 L 50 50" }),
32
+ /* @__PURE__ */ jsx("path", { d: "M 78 78 L 70 78 L 70 68 L 50 50" })
33
+ ]
34
+ }
35
+ )
36
+ ]
37
+ }
38
+ );
39
+ }
40
+
41
+ // src/BackToLauncher.tsx
42
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
43
+ var DEFAULT_LAUNCHER_URL = "https://xzibit-apps.vercel.app";
44
+ function BackToLauncher({
45
+ launcherUrl = DEFAULT_LAUNCHER_URL
46
+ }) {
47
+ const [hover, setHover] = useState(false);
48
+ const chevronColor = hover ? "#ffffff" : "rgba(255, 255, 255, 0.7)";
49
+ const textColor = hover ? "#ffffff" : "rgba(255, 255, 255, 0.85)";
50
+ const bgColor = hover ? "rgba(255, 255, 255, 0.07)" : "transparent";
51
+ return /* @__PURE__ */ jsxs2(
52
+ "a",
53
+ {
54
+ href: launcherUrl,
55
+ "aria-label": "Back to Xzibit Apps launcher",
56
+ onMouseEnter: () => setHover(true),
57
+ onMouseLeave: () => setHover(false),
58
+ onFocus: () => setHover(true),
59
+ onBlur: () => setHover(false),
60
+ style: {
61
+ display: "flex",
62
+ alignItems: "center",
63
+ gap: "8px",
64
+ padding: "0 1rem",
65
+ height: "44px",
66
+ fontSize: "15px",
67
+ fontWeight: 500,
68
+ color: textColor,
69
+ background: bgColor,
70
+ textDecoration: "none",
71
+ transition: "background 120ms, color 120ms"
72
+ },
73
+ children: [
74
+ /* @__PURE__ */ jsx2(
75
+ "svg",
76
+ {
77
+ width: 12,
78
+ height: 12,
79
+ viewBox: "0 0 12 12",
80
+ fill: "none",
81
+ "aria-hidden": "true",
82
+ children: /* @__PURE__ */ jsx2(
83
+ "path",
84
+ {
85
+ d: "M 8 2 L 3 6 L 8 10",
86
+ stroke: chevronColor,
87
+ strokeWidth: 1.8,
88
+ strokeLinecap: "round",
89
+ strokeLinejoin: "round"
90
+ }
91
+ )
92
+ }
93
+ ),
94
+ /* @__PURE__ */ jsx2(XzibitMark, { size: 28 }),
95
+ /* @__PURE__ */ jsx2("span", { children: "Xzibit Apps" })
96
+ ]
97
+ }
98
+ );
99
+ }
100
+
101
+ // src/AppsDropdown.tsx
102
+ import { useState as useState3, useEffect as useEffect2, useRef, Fragment } from "react";
103
+
104
+ // src/useApps.ts
105
+ import { useState as useState2, useEffect, useCallback } from "react";
106
+ function useApps(options = {}) {
107
+ const { endpoint = "/api/me/apps", lazy = false } = options;
108
+ const [apps, setApps] = useState2([]);
109
+ const [loading, setLoading] = useState2(false);
110
+ const [error, setError] = useState2(null);
111
+ const fetchApps = useCallback(async () => {
112
+ setLoading(true);
113
+ setError(null);
114
+ try {
115
+ const res = await fetch(endpoint, {
116
+ credentials: "include",
117
+ headers: { Accept: "application/json" }
118
+ });
119
+ if (!res.ok) {
120
+ throw new Error(`Failed to load apps (HTTP ${res.status})`);
121
+ }
122
+ const data = await res.json();
123
+ if (data.error) {
124
+ throw new Error(data.error);
125
+ }
126
+ setApps(data.apps ?? []);
127
+ } catch (err) {
128
+ console.error("[@xzibit/ui useApps] fetch failed:", err);
129
+ setError(err instanceof Error ? err.message : "Unknown error");
130
+ setApps([]);
131
+ } finally {
132
+ setLoading(false);
133
+ }
134
+ }, [endpoint]);
135
+ useEffect(() => {
136
+ if (!lazy) {
137
+ fetchApps();
138
+ }
139
+ }, [fetchApps, lazy]);
140
+ return { apps, loading, error, refetch: fetchApps };
141
+ }
142
+
143
+ // src/AppsDropdown.tsx
144
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
145
+ function AppsDropdown({ endpoint } = {}) {
146
+ const [open, setOpen] = useState3(false);
147
+ const { apps, loading, error } = useApps({ endpoint });
148
+ const buttonRef = useRef(null);
149
+ const panelRef = useRef(null);
150
+ useEffect2(() => {
151
+ if (!open) return;
152
+ const handler = (e) => {
153
+ if (e.key === "Escape") {
154
+ setOpen(false);
155
+ buttonRef.current?.focus();
156
+ }
157
+ };
158
+ document.addEventListener("keydown", handler);
159
+ return () => document.removeEventListener("keydown", handler);
160
+ }, [open]);
161
+ useEffect2(() => {
162
+ if (!open) return;
163
+ const handler = (e) => {
164
+ const target = e.target;
165
+ if (panelRef.current?.contains(target)) return;
166
+ if (buttonRef.current?.contains(target)) return;
167
+ setOpen(false);
168
+ };
169
+ document.addEventListener("mousedown", handler);
170
+ return () => document.removeEventListener("mousedown", handler);
171
+ }, [open]);
172
+ const grouped = groupAndSort(apps);
173
+ const hasApps = grouped.length > 0;
174
+ return /* @__PURE__ */ jsxs3("div", { style: { position: "relative" }, children: [
175
+ /* @__PURE__ */ jsxs3(
176
+ "button",
177
+ {
178
+ ref: buttonRef,
179
+ type: "button",
180
+ onClick: () => setOpen((o) => !o),
181
+ "aria-haspopup": "menu",
182
+ "aria-expanded": open,
183
+ style: {
184
+ display: "flex",
185
+ alignItems: "center",
186
+ gap: "6px",
187
+ padding: "0 1rem",
188
+ height: "44px",
189
+ background: open ? "rgba(255, 255, 255, 0.05)" : "transparent",
190
+ border: "none",
191
+ color: open ? "#ffffff" : "rgba(255, 255, 255, 0.7)",
192
+ fontSize: "13px",
193
+ fontWeight: 400,
194
+ cursor: "pointer",
195
+ fontFamily: "inherit",
196
+ transition: "background 120ms, color 120ms"
197
+ },
198
+ children: [
199
+ "Apps",
200
+ /* @__PURE__ */ jsx3("svg", { width: 10, height: 10, viewBox: "0 0 10 10", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx3(
201
+ "path",
202
+ {
203
+ d: open ? "M 1 7 L 5 3 L 9 7" : "M 1 3 L 5 7 L 9 3",
204
+ stroke: "currentColor",
205
+ strokeWidth: 1.5,
206
+ strokeLinecap: "round",
207
+ strokeLinejoin: "round"
208
+ }
209
+ ) })
210
+ ]
211
+ }
212
+ ),
213
+ open && /* @__PURE__ */ jsxs3(
214
+ "div",
215
+ {
216
+ ref: panelRef,
217
+ role: "menu",
218
+ style: {
219
+ position: "absolute",
220
+ top: "44px",
221
+ left: 0,
222
+ minWidth: "280px",
223
+ maxWidth: "360px",
224
+ maxHeight: "60vh",
225
+ overflowY: "auto",
226
+ background: "var(--xz-white, #ffffff)",
227
+ border: "1px solid var(--border, #E2E4E5)",
228
+ borderRadius: "8px",
229
+ boxShadow: "var(--shadow-card-hover, 0 4px 12px rgba(29,37,45,0.10), 0 12px 32px rgba(29,37,45,0.10))",
230
+ padding: "0.5rem",
231
+ zIndex: 110
232
+ },
233
+ children: [
234
+ loading && /* @__PURE__ */ jsx3(DropdownSkeleton, {}),
235
+ !loading && error && /* @__PURE__ */ jsx3(DropdownError, { message: error }),
236
+ !loading && !error && !hasApps && /* @__PURE__ */ jsx3(DropdownEmpty, {}),
237
+ !loading && !error && hasApps && /* @__PURE__ */ jsx3(Fragment2, { children: grouped.map((group, i) => /* @__PURE__ */ jsxs3(Fragment, { children: [
238
+ i > 0 && /* @__PURE__ */ jsx3(
239
+ "hr",
240
+ {
241
+ "aria-hidden": "true",
242
+ style: {
243
+ border: "none",
244
+ borderTop: "1px solid var(--border, #E2E4E5)",
245
+ margin: "8px 0"
246
+ }
247
+ }
248
+ ),
249
+ /* @__PURE__ */ jsx3(DropdownSection, { section: group.section, apps: group.apps })
250
+ ] }, group.section ?? "__no_section__")) })
251
+ ]
252
+ }
253
+ )
254
+ ] });
255
+ }
256
+ function groupAndSort(apps) {
257
+ const sectionMap = /* @__PURE__ */ new Map();
258
+ const sectionOrder = /* @__PURE__ */ new Map();
259
+ apps.forEach((app, i) => {
260
+ const key = app.section ?? null;
261
+ if (!sectionMap.has(key)) {
262
+ sectionMap.set(key, []);
263
+ sectionOrder.set(key, app.section_order ?? i);
264
+ }
265
+ sectionMap.get(key).push(app);
266
+ });
267
+ const groups = Array.from(sectionMap.entries()).map(
268
+ ([section, sectionApps]) => ({
269
+ section,
270
+ apps: [...sectionApps].sort(
271
+ (a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" })
272
+ )
273
+ })
274
+ );
275
+ groups.sort(
276
+ (a, b) => (sectionOrder.get(a.section) ?? Number.MAX_SAFE_INTEGER) - (sectionOrder.get(b.section) ?? Number.MAX_SAFE_INTEGER)
277
+ );
278
+ return groups;
279
+ }
280
+ function DropdownSection({
281
+ section,
282
+ apps
283
+ }) {
284
+ return /* @__PURE__ */ jsxs3("div", { children: [
285
+ section && /* @__PURE__ */ jsx3(
286
+ "div",
287
+ {
288
+ style: {
289
+ fontSize: "11px",
290
+ fontWeight: 500,
291
+ color: "var(--muted-foreground, #888A8B)",
292
+ letterSpacing: "0.06em",
293
+ textTransform: "uppercase",
294
+ padding: "0 0.75rem",
295
+ marginBottom: "4px"
296
+ },
297
+ children: section
298
+ }
299
+ ),
300
+ apps.map((app) => /* @__PURE__ */ jsx3(DropdownItem, { app }, app.name + app.url))
301
+ ] });
302
+ }
303
+ function DropdownItem({ app }) {
304
+ const [hover, setHover] = useState3(false);
305
+ return /* @__PURE__ */ jsx3(
306
+ "a",
307
+ {
308
+ href: app.url,
309
+ role: "menuitem",
310
+ onMouseEnter: () => setHover(true),
311
+ onMouseLeave: () => setHover(false),
312
+ style: {
313
+ display: "block",
314
+ padding: "0.5rem 0.75rem",
315
+ borderRadius: "4px",
316
+ fontSize: "14px",
317
+ fontWeight: 400,
318
+ color: "var(--foreground, #1D252D)",
319
+ textDecoration: "none",
320
+ background: hover ? "var(--xz-off-white, #F4F4F2)" : "transparent",
321
+ transition: "background 80ms"
322
+ },
323
+ children: app.name
324
+ }
325
+ );
326
+ }
327
+ function DropdownSkeleton() {
328
+ const widths = ["78%", "64%", "82%", "70%"];
329
+ return /* @__PURE__ */ jsxs3("div", { style: { padding: "0.5rem 0.75rem" }, children: [
330
+ widths.map((w, i) => /* @__PURE__ */ jsx3(
331
+ "div",
332
+ {
333
+ "aria-hidden": "true",
334
+ style: {
335
+ height: "14px",
336
+ background: "rgba(216, 218, 219, 0.4)",
337
+ borderRadius: "4px",
338
+ marginBottom: "8px",
339
+ width: w,
340
+ animation: "xzibit-pulse 1.4s ease-in-out infinite"
341
+ }
342
+ },
343
+ i
344
+ )),
345
+ /* @__PURE__ */ jsx3("style", { children: `@keyframes xzibit-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.5 } }` })
346
+ ] });
347
+ }
348
+ function DropdownError({ message }) {
349
+ return /* @__PURE__ */ jsxs3(
350
+ "div",
351
+ {
352
+ role: "alert",
353
+ style: {
354
+ padding: "0.75rem",
355
+ fontSize: "13px",
356
+ fontWeight: 400,
357
+ color: "var(--destructive, #C0392B)"
358
+ },
359
+ children: [
360
+ "Failed to load apps: ",
361
+ message
362
+ ]
363
+ }
364
+ );
365
+ }
366
+ function DropdownEmpty() {
367
+ return /* @__PURE__ */ jsx3(
368
+ "div",
369
+ {
370
+ style: {
371
+ padding: "0.75rem",
372
+ fontSize: "13px",
373
+ fontWeight: 400,
374
+ color: "var(--muted-foreground, #888A8B)"
375
+ },
376
+ children: "No other apps available"
377
+ }
378
+ );
379
+ }
380
+
381
+ // src/TopBar.tsx
382
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
383
+ function TopBar({
384
+ appName,
385
+ appHomeUrl = "/",
386
+ buildSha,
387
+ buildTimestamp,
388
+ launcherUrl,
389
+ appsEndpoint
390
+ }) {
391
+ return /* @__PURE__ */ jsxs4(
392
+ "header",
393
+ {
394
+ style: {
395
+ position: "fixed",
396
+ top: 0,
397
+ left: 0,
398
+ right: 0,
399
+ height: "44px",
400
+ zIndex: 100,
401
+ background: "var(--xz-charcoal, #252E38)",
402
+ borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
403
+ padding: "0 1rem",
404
+ display: "flex",
405
+ alignItems: "center"
406
+ },
407
+ children: [
408
+ /* @__PURE__ */ jsx4(
409
+ "a",
410
+ {
411
+ href: "#main",
412
+ style: {
413
+ position: "absolute",
414
+ left: "-9999px",
415
+ top: "auto",
416
+ width: "1px",
417
+ height: "1px",
418
+ overflow: "hidden"
419
+ },
420
+ children: "Skip to main content"
421
+ }
422
+ ),
423
+ /* @__PURE__ */ jsx4(BackToLauncher, { launcherUrl }),
424
+ /* @__PURE__ */ jsx4(VerticalSeparator, {}),
425
+ /* @__PURE__ */ jsx4(AppWordmark, { appName, appHomeUrl }),
426
+ /* @__PURE__ */ jsx4(VerticalSeparator, {}),
427
+ /* @__PURE__ */ jsx4(AppsDropdown, { endpoint: appsEndpoint }),
428
+ /* @__PURE__ */ jsx4(
429
+ "div",
430
+ {
431
+ style: {
432
+ marginLeft: "auto",
433
+ display: "flex",
434
+ alignItems: "center"
435
+ },
436
+ children: buildSha && /* @__PURE__ */ jsx4(BuildBadge, { sha: buildSha, timestamp: buildTimestamp })
437
+ }
438
+ )
439
+ ]
440
+ }
441
+ );
442
+ }
443
+ function VerticalSeparator() {
444
+ return /* @__PURE__ */ jsx4(
445
+ "div",
446
+ {
447
+ "aria-hidden": "true",
448
+ style: {
449
+ width: "1px",
450
+ height: "26px",
451
+ background: "rgba(255, 255, 255, 0.08)",
452
+ flexShrink: 0
453
+ }
454
+ }
455
+ );
456
+ }
457
+ function AppWordmark({
458
+ appName,
459
+ appHomeUrl
460
+ }) {
461
+ return /* @__PURE__ */ jsxs4(
462
+ "a",
463
+ {
464
+ href: appHomeUrl,
465
+ style: {
466
+ display: "flex",
467
+ alignItems: "center",
468
+ gap: "8px",
469
+ padding: "0 1rem",
470
+ height: "44px",
471
+ textDecoration: "none",
472
+ color: "#ffffff",
473
+ fontSize: "18px",
474
+ fontWeight: 500
475
+ },
476
+ children: [
477
+ /* @__PURE__ */ jsx4(
478
+ "span",
479
+ {
480
+ "aria-hidden": "true",
481
+ style: {
482
+ width: "4px",
483
+ height: "4px",
484
+ background: "var(--xz-teal, #19B1A1)",
485
+ borderRadius: "50%",
486
+ display: "inline-block",
487
+ flexShrink: 0
488
+ }
489
+ }
490
+ ),
491
+ /* @__PURE__ */ jsx4("span", { children: appName })
492
+ ]
493
+ }
494
+ );
495
+ }
496
+ function BuildBadge({
497
+ sha,
498
+ timestamp
499
+ }) {
500
+ return /* @__PURE__ */ jsxs4(
501
+ "div",
502
+ {
503
+ "aria-hidden": "true",
504
+ style: {
505
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
506
+ fontSize: "11px",
507
+ fontWeight: 400,
508
+ color: "rgba(255, 255, 255, 0.5)",
509
+ padding: "0 1rem"
510
+ },
511
+ children: [
512
+ sha,
513
+ timestamp && ` \xB7 Last updated ${timestamp}`
514
+ ]
515
+ }
516
+ );
517
+ }
518
+ export {
519
+ AppsDropdown,
520
+ BackToLauncher,
521
+ TopBar,
522
+ XzibitMark,
523
+ useApps
524
+ };