@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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/index.cjs +555 -0
- package/dist/index.d.cts +153 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +524 -0
- package/package.json +61 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AppsDropdown: () => AppsDropdown,
|
|
24
|
+
BackToLauncher: () => BackToLauncher,
|
|
25
|
+
TopBar: () => TopBar,
|
|
26
|
+
XzibitMark: () => XzibitMark,
|
|
27
|
+
useApps: () => useApps
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/BackToLauncher.tsx
|
|
32
|
+
var import_react = require("react");
|
|
33
|
+
|
|
34
|
+
// src/XzibitMark.tsx
|
|
35
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
36
|
+
function XzibitMark({ size = 28, className, ariaLabel }) {
|
|
37
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
38
|
+
"svg",
|
|
39
|
+
{
|
|
40
|
+
width: size,
|
|
41
|
+
height: size,
|
|
42
|
+
viewBox: "0 0 100 100",
|
|
43
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
44
|
+
role: ariaLabel ? "img" : "presentation",
|
|
45
|
+
"aria-label": ariaLabel,
|
|
46
|
+
"aria-hidden": ariaLabel ? void 0 : true,
|
|
47
|
+
className,
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: 0, y: 0, width: 100, height: 100, fill: "#000000" }),
|
|
50
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
51
|
+
"g",
|
|
52
|
+
{
|
|
53
|
+
stroke: "#FFFFFF",
|
|
54
|
+
strokeWidth: 9,
|
|
55
|
+
fill: "none",
|
|
56
|
+
strokeLinecap: "square",
|
|
57
|
+
strokeLinejoin: "miter",
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M 22 22 L 30 22 L 30 32 L 50 50" }),
|
|
60
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M 78 22 L 70 22 L 70 32 L 50 50" }),
|
|
61
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M 22 78 L 30 78 L 30 68 L 50 50" }),
|
|
62
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M 78 78 L 70 78 L 70 68 L 50 50" })
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/BackToLauncher.tsx
|
|
72
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
73
|
+
var DEFAULT_LAUNCHER_URL = "https://xzibit-apps.vercel.app";
|
|
74
|
+
function BackToLauncher({
|
|
75
|
+
launcherUrl = DEFAULT_LAUNCHER_URL
|
|
76
|
+
}) {
|
|
77
|
+
const [hover, setHover] = (0, import_react.useState)(false);
|
|
78
|
+
const chevronColor = hover ? "#ffffff" : "rgba(255, 255, 255, 0.7)";
|
|
79
|
+
const textColor = hover ? "#ffffff" : "rgba(255, 255, 255, 0.85)";
|
|
80
|
+
const bgColor = hover ? "rgba(255, 255, 255, 0.07)" : "transparent";
|
|
81
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
82
|
+
"a",
|
|
83
|
+
{
|
|
84
|
+
href: launcherUrl,
|
|
85
|
+
"aria-label": "Back to Xzibit Apps launcher",
|
|
86
|
+
onMouseEnter: () => setHover(true),
|
|
87
|
+
onMouseLeave: () => setHover(false),
|
|
88
|
+
onFocus: () => setHover(true),
|
|
89
|
+
onBlur: () => setHover(false),
|
|
90
|
+
style: {
|
|
91
|
+
display: "flex",
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
gap: "8px",
|
|
94
|
+
padding: "0 1rem",
|
|
95
|
+
height: "44px",
|
|
96
|
+
fontSize: "15px",
|
|
97
|
+
fontWeight: 500,
|
|
98
|
+
color: textColor,
|
|
99
|
+
background: bgColor,
|
|
100
|
+
textDecoration: "none",
|
|
101
|
+
transition: "background 120ms, color 120ms"
|
|
102
|
+
},
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
105
|
+
"svg",
|
|
106
|
+
{
|
|
107
|
+
width: 12,
|
|
108
|
+
height: 12,
|
|
109
|
+
viewBox: "0 0 12 12",
|
|
110
|
+
fill: "none",
|
|
111
|
+
"aria-hidden": "true",
|
|
112
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
113
|
+
"path",
|
|
114
|
+
{
|
|
115
|
+
d: "M 8 2 L 3 6 L 8 10",
|
|
116
|
+
stroke: chevronColor,
|
|
117
|
+
strokeWidth: 1.8,
|
|
118
|
+
strokeLinecap: "round",
|
|
119
|
+
strokeLinejoin: "round"
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
),
|
|
124
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(XzibitMark, { size: 28 }),
|
|
125
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Xzibit Apps" })
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/AppsDropdown.tsx
|
|
132
|
+
var import_react3 = require("react");
|
|
133
|
+
|
|
134
|
+
// src/useApps.ts
|
|
135
|
+
var import_react2 = require("react");
|
|
136
|
+
function useApps(options = {}) {
|
|
137
|
+
const { endpoint = "/api/me/apps", lazy = false } = options;
|
|
138
|
+
const [apps, setApps] = (0, import_react2.useState)([]);
|
|
139
|
+
const [loading, setLoading] = (0, import_react2.useState)(false);
|
|
140
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
141
|
+
const fetchApps = (0, import_react2.useCallback)(async () => {
|
|
142
|
+
setLoading(true);
|
|
143
|
+
setError(null);
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch(endpoint, {
|
|
146
|
+
credentials: "include",
|
|
147
|
+
headers: { Accept: "application/json" }
|
|
148
|
+
});
|
|
149
|
+
if (!res.ok) {
|
|
150
|
+
throw new Error(`Failed to load apps (HTTP ${res.status})`);
|
|
151
|
+
}
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
if (data.error) {
|
|
154
|
+
throw new Error(data.error);
|
|
155
|
+
}
|
|
156
|
+
setApps(data.apps ?? []);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.error("[@xzibit/ui useApps] fetch failed:", err);
|
|
159
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
160
|
+
setApps([]);
|
|
161
|
+
} finally {
|
|
162
|
+
setLoading(false);
|
|
163
|
+
}
|
|
164
|
+
}, [endpoint]);
|
|
165
|
+
(0, import_react2.useEffect)(() => {
|
|
166
|
+
if (!lazy) {
|
|
167
|
+
fetchApps();
|
|
168
|
+
}
|
|
169
|
+
}, [fetchApps, lazy]);
|
|
170
|
+
return { apps, loading, error, refetch: fetchApps };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/AppsDropdown.tsx
|
|
174
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
175
|
+
function AppsDropdown({ endpoint } = {}) {
|
|
176
|
+
const [open, setOpen] = (0, import_react3.useState)(false);
|
|
177
|
+
const { apps, loading, error } = useApps({ endpoint });
|
|
178
|
+
const buttonRef = (0, import_react3.useRef)(null);
|
|
179
|
+
const panelRef = (0, import_react3.useRef)(null);
|
|
180
|
+
(0, import_react3.useEffect)(() => {
|
|
181
|
+
if (!open) return;
|
|
182
|
+
const handler = (e) => {
|
|
183
|
+
if (e.key === "Escape") {
|
|
184
|
+
setOpen(false);
|
|
185
|
+
buttonRef.current?.focus();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
document.addEventListener("keydown", handler);
|
|
189
|
+
return () => document.removeEventListener("keydown", handler);
|
|
190
|
+
}, [open]);
|
|
191
|
+
(0, import_react3.useEffect)(() => {
|
|
192
|
+
if (!open) return;
|
|
193
|
+
const handler = (e) => {
|
|
194
|
+
const target = e.target;
|
|
195
|
+
if (panelRef.current?.contains(target)) return;
|
|
196
|
+
if (buttonRef.current?.contains(target)) return;
|
|
197
|
+
setOpen(false);
|
|
198
|
+
};
|
|
199
|
+
document.addEventListener("mousedown", handler);
|
|
200
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
201
|
+
}, [open]);
|
|
202
|
+
const grouped = groupAndSort(apps);
|
|
203
|
+
const hasApps = grouped.length > 0;
|
|
204
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { position: "relative" }, children: [
|
|
205
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
206
|
+
"button",
|
|
207
|
+
{
|
|
208
|
+
ref: buttonRef,
|
|
209
|
+
type: "button",
|
|
210
|
+
onClick: () => setOpen((o) => !o),
|
|
211
|
+
"aria-haspopup": "menu",
|
|
212
|
+
"aria-expanded": open,
|
|
213
|
+
style: {
|
|
214
|
+
display: "flex",
|
|
215
|
+
alignItems: "center",
|
|
216
|
+
gap: "6px",
|
|
217
|
+
padding: "0 1rem",
|
|
218
|
+
height: "44px",
|
|
219
|
+
background: open ? "rgba(255, 255, 255, 0.05)" : "transparent",
|
|
220
|
+
border: "none",
|
|
221
|
+
color: open ? "#ffffff" : "rgba(255, 255, 255, 0.7)",
|
|
222
|
+
fontSize: "13px",
|
|
223
|
+
fontWeight: 400,
|
|
224
|
+
cursor: "pointer",
|
|
225
|
+
fontFamily: "inherit",
|
|
226
|
+
transition: "background 120ms, color 120ms"
|
|
227
|
+
},
|
|
228
|
+
children: [
|
|
229
|
+
"Apps",
|
|
230
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: 10, height: 10, viewBox: "0 0 10 10", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
231
|
+
"path",
|
|
232
|
+
{
|
|
233
|
+
d: open ? "M 1 7 L 5 3 L 9 7" : "M 1 3 L 5 7 L 9 3",
|
|
234
|
+
stroke: "currentColor",
|
|
235
|
+
strokeWidth: 1.5,
|
|
236
|
+
strokeLinecap: "round",
|
|
237
|
+
strokeLinejoin: "round"
|
|
238
|
+
}
|
|
239
|
+
) })
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
),
|
|
243
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
244
|
+
"div",
|
|
245
|
+
{
|
|
246
|
+
ref: panelRef,
|
|
247
|
+
role: "menu",
|
|
248
|
+
style: {
|
|
249
|
+
position: "absolute",
|
|
250
|
+
top: "44px",
|
|
251
|
+
left: 0,
|
|
252
|
+
minWidth: "280px",
|
|
253
|
+
maxWidth: "360px",
|
|
254
|
+
maxHeight: "60vh",
|
|
255
|
+
overflowY: "auto",
|
|
256
|
+
background: "var(--xz-white, #ffffff)",
|
|
257
|
+
border: "1px solid var(--border, #E2E4E5)",
|
|
258
|
+
borderRadius: "8px",
|
|
259
|
+
boxShadow: "var(--shadow-card-hover, 0 4px 12px rgba(29,37,45,0.10), 0 12px 32px rgba(29,37,45,0.10))",
|
|
260
|
+
padding: "0.5rem",
|
|
261
|
+
zIndex: 110
|
|
262
|
+
},
|
|
263
|
+
children: [
|
|
264
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DropdownSkeleton, {}),
|
|
265
|
+
!loading && error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DropdownError, { message: error }),
|
|
266
|
+
!loading && !error && !hasApps && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DropdownEmpty, {}),
|
|
267
|
+
!loading && !error && hasApps && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: grouped.map((group, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react3.Fragment, { children: [
|
|
268
|
+
i > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
269
|
+
"hr",
|
|
270
|
+
{
|
|
271
|
+
"aria-hidden": "true",
|
|
272
|
+
style: {
|
|
273
|
+
border: "none",
|
|
274
|
+
borderTop: "1px solid var(--border, #E2E4E5)",
|
|
275
|
+
margin: "8px 0"
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
),
|
|
279
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DropdownSection, { section: group.section, apps: group.apps })
|
|
280
|
+
] }, group.section ?? "__no_section__")) })
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
] });
|
|
285
|
+
}
|
|
286
|
+
function groupAndSort(apps) {
|
|
287
|
+
const sectionMap = /* @__PURE__ */ new Map();
|
|
288
|
+
const sectionOrder = /* @__PURE__ */ new Map();
|
|
289
|
+
apps.forEach((app, i) => {
|
|
290
|
+
const key = app.section ?? null;
|
|
291
|
+
if (!sectionMap.has(key)) {
|
|
292
|
+
sectionMap.set(key, []);
|
|
293
|
+
sectionOrder.set(key, app.section_order ?? i);
|
|
294
|
+
}
|
|
295
|
+
sectionMap.get(key).push(app);
|
|
296
|
+
});
|
|
297
|
+
const groups = Array.from(sectionMap.entries()).map(
|
|
298
|
+
([section, sectionApps]) => ({
|
|
299
|
+
section,
|
|
300
|
+
apps: [...sectionApps].sort(
|
|
301
|
+
(a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base" })
|
|
302
|
+
)
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
groups.sort(
|
|
306
|
+
(a, b) => (sectionOrder.get(a.section) ?? Number.MAX_SAFE_INTEGER) - (sectionOrder.get(b.section) ?? Number.MAX_SAFE_INTEGER)
|
|
307
|
+
);
|
|
308
|
+
return groups;
|
|
309
|
+
}
|
|
310
|
+
function DropdownSection({
|
|
311
|
+
section,
|
|
312
|
+
apps
|
|
313
|
+
}) {
|
|
314
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
|
|
315
|
+
section && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
316
|
+
"div",
|
|
317
|
+
{
|
|
318
|
+
style: {
|
|
319
|
+
fontSize: "11px",
|
|
320
|
+
fontWeight: 500,
|
|
321
|
+
color: "var(--muted-foreground, #888A8B)",
|
|
322
|
+
letterSpacing: "0.06em",
|
|
323
|
+
textTransform: "uppercase",
|
|
324
|
+
padding: "0 0.75rem",
|
|
325
|
+
marginBottom: "4px"
|
|
326
|
+
},
|
|
327
|
+
children: section
|
|
328
|
+
}
|
|
329
|
+
),
|
|
330
|
+
apps.map((app) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DropdownItem, { app }, app.name + app.url))
|
|
331
|
+
] });
|
|
332
|
+
}
|
|
333
|
+
function DropdownItem({ app }) {
|
|
334
|
+
const [hover, setHover] = (0, import_react3.useState)(false);
|
|
335
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
336
|
+
"a",
|
|
337
|
+
{
|
|
338
|
+
href: app.url,
|
|
339
|
+
role: "menuitem",
|
|
340
|
+
onMouseEnter: () => setHover(true),
|
|
341
|
+
onMouseLeave: () => setHover(false),
|
|
342
|
+
style: {
|
|
343
|
+
display: "block",
|
|
344
|
+
padding: "0.5rem 0.75rem",
|
|
345
|
+
borderRadius: "4px",
|
|
346
|
+
fontSize: "14px",
|
|
347
|
+
fontWeight: 400,
|
|
348
|
+
color: "var(--foreground, #1D252D)",
|
|
349
|
+
textDecoration: "none",
|
|
350
|
+
background: hover ? "var(--xz-off-white, #F4F4F2)" : "transparent",
|
|
351
|
+
transition: "background 80ms"
|
|
352
|
+
},
|
|
353
|
+
children: app.name
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
function DropdownSkeleton() {
|
|
358
|
+
const widths = ["78%", "64%", "82%", "70%"];
|
|
359
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { padding: "0.5rem 0.75rem" }, children: [
|
|
360
|
+
widths.map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
361
|
+
"div",
|
|
362
|
+
{
|
|
363
|
+
"aria-hidden": "true",
|
|
364
|
+
style: {
|
|
365
|
+
height: "14px",
|
|
366
|
+
background: "rgba(216, 218, 219, 0.4)",
|
|
367
|
+
borderRadius: "4px",
|
|
368
|
+
marginBottom: "8px",
|
|
369
|
+
width: w,
|
|
370
|
+
animation: "xzibit-pulse 1.4s ease-in-out infinite"
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
i
|
|
374
|
+
)),
|
|
375
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `@keyframes xzibit-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.5 } }` })
|
|
376
|
+
] });
|
|
377
|
+
}
|
|
378
|
+
function DropdownError({ message }) {
|
|
379
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
380
|
+
"div",
|
|
381
|
+
{
|
|
382
|
+
role: "alert",
|
|
383
|
+
style: {
|
|
384
|
+
padding: "0.75rem",
|
|
385
|
+
fontSize: "13px",
|
|
386
|
+
fontWeight: 400,
|
|
387
|
+
color: "var(--destructive, #C0392B)"
|
|
388
|
+
},
|
|
389
|
+
children: [
|
|
390
|
+
"Failed to load apps: ",
|
|
391
|
+
message
|
|
392
|
+
]
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
function DropdownEmpty() {
|
|
397
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
398
|
+
"div",
|
|
399
|
+
{
|
|
400
|
+
style: {
|
|
401
|
+
padding: "0.75rem",
|
|
402
|
+
fontSize: "13px",
|
|
403
|
+
fontWeight: 400,
|
|
404
|
+
color: "var(--muted-foreground, #888A8B)"
|
|
405
|
+
},
|
|
406
|
+
children: "No other apps available"
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/TopBar.tsx
|
|
412
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
413
|
+
function TopBar({
|
|
414
|
+
appName,
|
|
415
|
+
appHomeUrl = "/",
|
|
416
|
+
buildSha,
|
|
417
|
+
buildTimestamp,
|
|
418
|
+
launcherUrl,
|
|
419
|
+
appsEndpoint
|
|
420
|
+
}) {
|
|
421
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
422
|
+
"header",
|
|
423
|
+
{
|
|
424
|
+
style: {
|
|
425
|
+
position: "fixed",
|
|
426
|
+
top: 0,
|
|
427
|
+
left: 0,
|
|
428
|
+
right: 0,
|
|
429
|
+
height: "44px",
|
|
430
|
+
zIndex: 100,
|
|
431
|
+
background: "var(--xz-charcoal, #252E38)",
|
|
432
|
+
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
|
|
433
|
+
padding: "0 1rem",
|
|
434
|
+
display: "flex",
|
|
435
|
+
alignItems: "center"
|
|
436
|
+
},
|
|
437
|
+
children: [
|
|
438
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
439
|
+
"a",
|
|
440
|
+
{
|
|
441
|
+
href: "#main",
|
|
442
|
+
style: {
|
|
443
|
+
position: "absolute",
|
|
444
|
+
left: "-9999px",
|
|
445
|
+
top: "auto",
|
|
446
|
+
width: "1px",
|
|
447
|
+
height: "1px",
|
|
448
|
+
overflow: "hidden"
|
|
449
|
+
},
|
|
450
|
+
children: "Skip to main content"
|
|
451
|
+
}
|
|
452
|
+
),
|
|
453
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BackToLauncher, { launcherUrl }),
|
|
454
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(VerticalSeparator, {}),
|
|
455
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AppWordmark, { appName, appHomeUrl }),
|
|
456
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(VerticalSeparator, {}),
|
|
457
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(AppsDropdown, { endpoint: appsEndpoint }),
|
|
458
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
459
|
+
"div",
|
|
460
|
+
{
|
|
461
|
+
style: {
|
|
462
|
+
marginLeft: "auto",
|
|
463
|
+
display: "flex",
|
|
464
|
+
alignItems: "center"
|
|
465
|
+
},
|
|
466
|
+
children: buildSha && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BuildBadge, { sha: buildSha, timestamp: buildTimestamp })
|
|
467
|
+
}
|
|
468
|
+
)
|
|
469
|
+
]
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
function VerticalSeparator() {
|
|
474
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
475
|
+
"div",
|
|
476
|
+
{
|
|
477
|
+
"aria-hidden": "true",
|
|
478
|
+
style: {
|
|
479
|
+
width: "1px",
|
|
480
|
+
height: "26px",
|
|
481
|
+
background: "rgba(255, 255, 255, 0.08)",
|
|
482
|
+
flexShrink: 0
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
function AppWordmark({
|
|
488
|
+
appName,
|
|
489
|
+
appHomeUrl
|
|
490
|
+
}) {
|
|
491
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
492
|
+
"a",
|
|
493
|
+
{
|
|
494
|
+
href: appHomeUrl,
|
|
495
|
+
style: {
|
|
496
|
+
display: "flex",
|
|
497
|
+
alignItems: "center",
|
|
498
|
+
gap: "8px",
|
|
499
|
+
padding: "0 1rem",
|
|
500
|
+
height: "44px",
|
|
501
|
+
textDecoration: "none",
|
|
502
|
+
color: "#ffffff",
|
|
503
|
+
fontSize: "18px",
|
|
504
|
+
fontWeight: 500
|
|
505
|
+
},
|
|
506
|
+
children: [
|
|
507
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
508
|
+
"span",
|
|
509
|
+
{
|
|
510
|
+
"aria-hidden": "true",
|
|
511
|
+
style: {
|
|
512
|
+
width: "4px",
|
|
513
|
+
height: "4px",
|
|
514
|
+
background: "var(--xz-teal, #19B1A1)",
|
|
515
|
+
borderRadius: "50%",
|
|
516
|
+
display: "inline-block",
|
|
517
|
+
flexShrink: 0
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
),
|
|
521
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: appName })
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
function BuildBadge({
|
|
527
|
+
sha,
|
|
528
|
+
timestamp
|
|
529
|
+
}) {
|
|
530
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
531
|
+
"div",
|
|
532
|
+
{
|
|
533
|
+
"aria-hidden": "true",
|
|
534
|
+
style: {
|
|
535
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
536
|
+
fontSize: "11px",
|
|
537
|
+
fontWeight: 400,
|
|
538
|
+
color: "rgba(255, 255, 255, 0.5)",
|
|
539
|
+
padding: "0 1rem"
|
|
540
|
+
},
|
|
541
|
+
children: [
|
|
542
|
+
sha,
|
|
543
|
+
timestamp && ` \xB7 Last updated ${timestamp}`
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
549
|
+
0 && (module.exports = {
|
|
550
|
+
AppsDropdown,
|
|
551
|
+
BackToLauncher,
|
|
552
|
+
TopBar,
|
|
553
|
+
XzibitMark,
|
|
554
|
+
useApps
|
|
555
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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 };
|