@better-forge/ui 0.0.0-stage → 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.
Files changed (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -3
  3. package/dist/api/client.d.ts +42 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +130 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/components/ConfirmIdentityModal.d.ts +18 -0
  8. package/dist/components/ConfirmIdentityModal.d.ts.map +1 -0
  9. package/dist/components/ConfirmIdentityModal.js +30 -0
  10. package/dist/components/ConfirmIdentityModal.js.map +1 -0
  11. package/dist/components/ConfirmModal.d.ts +18 -0
  12. package/dist/components/ConfirmModal.d.ts.map +1 -0
  13. package/dist/components/ConfirmModal.js +11 -0
  14. package/dist/components/ConfirmModal.js.map +1 -0
  15. package/dist/components/CopyButton.d.ts +11 -0
  16. package/dist/components/CopyButton.d.ts.map +1 -0
  17. package/dist/components/CopyButton.js +44 -0
  18. package/dist/components/CopyButton.js.map +1 -0
  19. package/dist/components/CredentialSection.d.ts +38 -0
  20. package/dist/components/CredentialSection.d.ts.map +1 -0
  21. package/dist/components/CredentialSection.js +69 -0
  22. package/dist/components/CredentialSection.js.map +1 -0
  23. package/dist/components/ErrorBoundary.d.ts +18 -0
  24. package/dist/components/ErrorBoundary.d.ts.map +1 -0
  25. package/dist/components/ErrorBoundary.js +25 -0
  26. package/dist/components/ErrorBoundary.js.map +1 -0
  27. package/dist/components/ModalShell.d.ts +58 -0
  28. package/dist/components/ModalShell.d.ts.map +1 -0
  29. package/dist/components/ModalShell.js +92 -0
  30. package/dist/components/ModalShell.js.map +1 -0
  31. package/dist/components/ResetTwoFactorModal.d.ts +14 -0
  32. package/dist/components/ResetTwoFactorModal.d.ts.map +1 -0
  33. package/dist/components/ResetTwoFactorModal.js +16 -0
  34. package/dist/components/ResetTwoFactorModal.js.map +1 -0
  35. package/dist/components/SidePanel.d.ts +35 -0
  36. package/dist/components/SidePanel.d.ts.map +1 -0
  37. package/dist/components/SidePanel.js +19 -0
  38. package/dist/components/SidePanel.js.map +1 -0
  39. package/dist/components/SingleSignOnModal.d.ts +4 -0
  40. package/dist/components/SingleSignOnModal.d.ts.map +1 -0
  41. package/dist/components/SingleSignOnModal.js +64 -0
  42. package/dist/components/SingleSignOnModal.js.map +1 -0
  43. package/dist/components/SsoConfirmation.d.ts +22 -0
  44. package/dist/components/SsoConfirmation.d.ts.map +1 -0
  45. package/dist/components/SsoConfirmation.js +87 -0
  46. package/dist/components/SsoConfirmation.js.map +1 -0
  47. package/dist/components/VaultTypePicker.d.ts +13 -0
  48. package/dist/components/VaultTypePicker.d.ts.map +1 -0
  49. package/dist/components/VaultTypePicker.js +62 -0
  50. package/dist/components/VaultTypePicker.js.map +1 -0
  51. package/dist/components/WhatsNew.d.ts +18 -0
  52. package/dist/components/WhatsNew.d.ts.map +1 -0
  53. package/dist/components/WhatsNew.js +50 -0
  54. package/dist/components/WhatsNew.js.map +1 -0
  55. package/dist/components/formParts.d.ts +40 -0
  56. package/dist/components/formParts.d.ts.map +1 -0
  57. package/dist/components/formParts.js +31 -0
  58. package/dist/components/formParts.js.map +1 -0
  59. package/dist/components/ui/Alert.d.ts +15 -0
  60. package/dist/components/ui/Alert.d.ts.map +1 -0
  61. package/dist/components/ui/Alert.js +14 -0
  62. package/dist/components/ui/Alert.js.map +1 -0
  63. package/dist/components/ui/DataTable.d.ts +52 -0
  64. package/dist/components/ui/DataTable.d.ts.map +1 -0
  65. package/dist/components/ui/DataTable.js +112 -0
  66. package/dist/components/ui/DataTable.js.map +1 -0
  67. package/dist/components/ui/Field.d.ts +27 -0
  68. package/dist/components/ui/Field.d.ts.map +1 -0
  69. package/dist/components/ui/Field.js +25 -0
  70. package/dist/components/ui/Field.js.map +1 -0
  71. package/dist/components/ui/PageHeader.d.ts +18 -0
  72. package/dist/components/ui/PageHeader.d.ts.map +1 -0
  73. package/dist/components/ui/PageHeader.js +10 -0
  74. package/dist/components/ui/PageHeader.js.map +1 -0
  75. package/dist/components/ui/Pagination.d.ts +21 -0
  76. package/dist/components/ui/Pagination.d.ts.map +1 -0
  77. package/dist/components/ui/Pagination.js +10 -0
  78. package/dist/components/ui/Pagination.js.map +1 -0
  79. package/dist/components/ui/QuickStartPanel.d.ts +21 -0
  80. package/dist/components/ui/QuickStartPanel.d.ts.map +1 -0
  81. package/dist/components/ui/QuickStartPanel.js +11 -0
  82. package/dist/components/ui/QuickStartPanel.js.map +1 -0
  83. package/dist/components/ui/StateViews.d.ts +50 -0
  84. package/dist/components/ui/StateViews.d.ts.map +1 -0
  85. package/dist/components/ui/StateViews.js +33 -0
  86. package/dist/components/ui/StateViews.js.map +1 -0
  87. package/dist/components/ui/Switch.d.ts +16 -0
  88. package/dist/components/ui/Switch.d.ts.map +1 -0
  89. package/dist/components/ui/Switch.js +15 -0
  90. package/dist/components/ui/Switch.js.map +1 -0
  91. package/dist/components/ui/Toast.d.ts +20 -0
  92. package/dist/components/ui/Toast.d.ts.map +1 -0
  93. package/dist/components/ui/Toast.js +46 -0
  94. package/dist/components/ui/Toast.js.map +1 -0
  95. package/dist/components/ui/index.d.ts +10 -0
  96. package/dist/components/ui/index.d.ts.map +1 -0
  97. package/dist/components/ui/index.js +10 -0
  98. package/dist/components/ui/index.js.map +1 -0
  99. package/dist/config.d.ts +46 -0
  100. package/dist/config.d.ts.map +1 -0
  101. package/dist/config.js +12 -0
  102. package/dist/config.js.map +1 -0
  103. package/dist/hooks/useDarkMode.d.ts +2 -0
  104. package/dist/hooks/useDarkMode.d.ts.map +1 -0
  105. package/dist/hooks/useDarkMode.js +37 -0
  106. package/dist/hooks/useDarkMode.js.map +1 -0
  107. package/dist/hooks/useDebouncedValue.d.ts +6 -0
  108. package/dist/hooks/useDebouncedValue.d.ts.map +1 -0
  109. package/dist/hooks/useDebouncedValue.js +19 -0
  110. package/dist/hooks/useDebouncedValue.js.map +1 -0
  111. package/dist/index.d.ts +36 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +36 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/lib/changelog.d.ts +32 -0
  116. package/dist/lib/changelog.d.ts.map +1 -0
  117. package/dist/lib/changelog.js +71 -0
  118. package/dist/lib/changelog.js.map +1 -0
  119. package/dist/lib/dateFormat.d.ts +24 -0
  120. package/dist/lib/dateFormat.d.ts.map +1 -0
  121. package/dist/lib/dateFormat.js +67 -0
  122. package/dist/lib/dateFormat.js.map +1 -0
  123. package/dist/navigation.d.ts +5 -0
  124. package/dist/navigation.d.ts.map +1 -0
  125. package/dist/navigation.js +7 -0
  126. package/dist/navigation.js.map +1 -0
  127. package/dist/pages/ApiTokens.d.ts +9 -0
  128. package/dist/pages/ApiTokens.d.ts.map +1 -0
  129. package/dist/pages/ApiTokens.js +116 -0
  130. package/dist/pages/ApiTokens.js.map +1 -0
  131. package/dist/pages/AuditLog.d.ts +14 -0
  132. package/dist/pages/AuditLog.d.ts.map +1 -0
  133. package/dist/pages/AuditLog.js +104 -0
  134. package/dist/pages/AuditLog.js.map +1 -0
  135. package/dist/pages/Backups.d.ts +2 -0
  136. package/dist/pages/Backups.d.ts.map +1 -0
  137. package/dist/pages/Backups.js +118 -0
  138. package/dist/pages/Backups.js.map +1 -0
  139. package/dist/pages/ChangePassword.d.ts +2 -0
  140. package/dist/pages/ChangePassword.d.ts.map +1 -0
  141. package/dist/pages/ChangePassword.js +40 -0
  142. package/dist/pages/ChangePassword.js.map +1 -0
  143. package/dist/pages/Login.d.ts +12 -0
  144. package/dist/pages/Login.d.ts.map +1 -0
  145. package/dist/pages/Login.js +69 -0
  146. package/dist/pages/Login.js.map +1 -0
  147. package/dist/pages/Settings.d.ts +2 -0
  148. package/dist/pages/Settings.d.ts.map +1 -0
  149. package/dist/pages/Settings.js +149 -0
  150. package/dist/pages/Settings.js.map +1 -0
  151. package/dist/pages/Setup.d.ts +2 -0
  152. package/dist/pages/Setup.d.ts.map +1 -0
  153. package/dist/pages/Setup.js +113 -0
  154. package/dist/pages/Setup.js.map +1 -0
  155. package/dist/pages/SsoConfirm.d.ts +3 -0
  156. package/dist/pages/SsoConfirm.d.ts.map +1 -0
  157. package/dist/pages/SsoConfirm.js +20 -0
  158. package/dist/pages/SsoConfirm.js.map +1 -0
  159. package/dist/pages/SsoTestResult.d.ts +2 -0
  160. package/dist/pages/SsoTestResult.d.ts.map +1 -0
  161. package/dist/pages/SsoTestResult.js +49 -0
  162. package/dist/pages/SsoTestResult.js.map +1 -0
  163. package/dist/pages/Users.d.ts +22 -0
  164. package/dist/pages/Users.d.ts.map +1 -0
  165. package/dist/pages/Users.js +124 -0
  166. package/dist/pages/Users.js.map +1 -0
  167. package/dist/pages/Vault.d.ts +2 -0
  168. package/dist/pages/Vault.d.ts.map +1 -0
  169. package/dist/pages/Vault.js +372 -0
  170. package/dist/pages/Vault.js.map +1 -0
  171. package/dist/shell/AdminShell.d.ts +46 -0
  172. package/dist/shell/AdminShell.d.ts.map +1 -0
  173. package/dist/shell/AdminShell.js +76 -0
  174. package/dist/shell/AdminShell.js.map +1 -0
  175. package/dist/shell/guards.d.ts +28 -0
  176. package/dist/shell/guards.d.ts.map +1 -0
  177. package/dist/shell/guards.js +71 -0
  178. package/dist/shell/guards.js.map +1 -0
  179. package/icons.json +170 -0
  180. package/package.json +71 -6
  181. package/src/api/client.ts +139 -0
  182. package/src/components/ConfirmIdentityModal.tsx +82 -0
  183. package/src/components/ConfirmModal.tsx +83 -0
  184. package/src/components/CopyButton.tsx +71 -0
  185. package/src/components/CredentialSection.tsx +270 -0
  186. package/src/components/ErrorBoundary.tsx +47 -0
  187. package/src/components/ModalShell.tsx +160 -0
  188. package/src/components/ResetTwoFactorModal.tsx +74 -0
  189. package/src/components/SidePanel.tsx +96 -0
  190. package/src/components/SingleSignOnModal.tsx +161 -0
  191. package/src/components/SsoConfirmation.tsx +96 -0
  192. package/src/components/VaultTypePicker.tsx +131 -0
  193. package/src/components/WhatsNew.tsx +84 -0
  194. package/src/components/formParts.tsx +59 -0
  195. package/src/components/ui/Alert.tsx +45 -0
  196. package/src/components/ui/DataTable.tsx +225 -0
  197. package/src/components/ui/Field.tsx +63 -0
  198. package/src/components/ui/PageHeader.tsx +36 -0
  199. package/src/components/ui/Pagination.tsx +37 -0
  200. package/src/components/ui/QuickStartPanel.tsx +57 -0
  201. package/src/components/ui/StateViews.tsx +135 -0
  202. package/src/components/ui/Switch.tsx +65 -0
  203. package/src/components/ui/Toast.tsx +104 -0
  204. package/src/components/ui/index.ts +9 -0
  205. package/src/config.ts +46 -0
  206. package/src/hooks/useDarkMode.ts +38 -0
  207. package/src/hooks/useDebouncedValue.ts +22 -0
  208. package/src/index.ts +42 -0
  209. package/src/lib/changelog.ts +93 -0
  210. package/src/lib/dateFormat.ts +90 -0
  211. package/src/navigation.ts +6 -0
  212. package/src/pages/ApiTokens.tsx +361 -0
  213. package/src/pages/AuditLog.tsx +388 -0
  214. package/src/pages/Backups.tsx +170 -0
  215. package/src/pages/ChangePassword.tsx +84 -0
  216. package/src/pages/Login.tsx +275 -0
  217. package/src/pages/Settings.tsx +264 -0
  218. package/src/pages/Setup.tsx +438 -0
  219. package/src/pages/SsoConfirm.tsx +26 -0
  220. package/src/pages/SsoTestResult.tsx +104 -0
  221. package/src/pages/Users.tsx +426 -0
  222. package/src/pages/Vault.tsx +1036 -0
  223. package/src/shell/AdminShell.tsx +256 -0
  224. package/src/shell/guards.tsx +88 -0
  225. package/styles/base.css +521 -0
  226. package/styles/fonts/inter-latin-1.woff2 +0 -0
  227. package/styles/fonts/inter-latin-ext.woff2 +0 -0
  228. package/styles/fonts/jetbrains-mono-latin-3.woff2 +0 -0
  229. package/styles/fonts/jetbrains-mono-latin-ext-2.woff2 +0 -0
  230. package/styles/fonts/manrope-latin-5.woff2 +0 -0
  231. package/styles/fonts/manrope-latin-ext-4.woff2 +0 -0
  232. package/styles/fonts/material-symbols-outlined-fill.woff2 +0 -0
  233. package/styles/icons.css +4 -0
  234. package/tailwind-preset.js +79 -0
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment, Suspense, useCallback, useEffect, useRef, useState } from 'react';
3
+ import { Outlet, NavLink, useNavigate, useLocation, Link } from 'react-router-dom';
4
+ import { api, clearSession, getCurrentUser } from '../api/client';
5
+ import { LoadingState } from '../components/ui/StateViews';
6
+ import { WhatsNew, markWhatsNewSeen, pendingWhatsNew } from '../components/WhatsNew';
7
+ import { useDarkMode } from '../hooks/useDarkMode';
8
+ import { useDatePrefs } from '../lib/dateFormat';
9
+ const ICON_STYLE = { fontSize: '20px' };
10
+ function Brand({ brand }) {
11
+ return (_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsx("img", { src: brand.iconUrl, alt: brand.name, style: { width: '40px', height: '40px', objectFit: 'contain', flexShrink: 0 } }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-headline font-bold text-base leading-none", style: { color: 'var(--m3-on-surface)' }, children: brand.title }), _jsx("p", { className: "text-xs leading-none mt-1", style: { color: 'var(--m3-secondary)' }, children: brand.subtitle })] })] }));
12
+ }
13
+ /** Section links plus the Settings / theme / logout footer, shared by the desktop sidebar and the mobile drawer. */
14
+ function NavContent({ config, sections, isDark, onToggleDark, onLogout }) {
15
+ return (_jsxs(_Fragment, { children: [_jsx("nav", { "aria-label": "Main", className: "flex-1 px-3 overflow-y-auto pb-2", children: sections.map((section, si) => (_jsxs("div", { className: si > 0 ? 'mt-4' : '', children: [_jsx("p", { className: "px-4 pb-1 font-mono uppercase tracking-widest", style: { fontSize: '10px', color: 'var(--m3-outline)', letterSpacing: '0.1em' }, children: section.label }), _jsx("div", { className: "space-y-0.5", children: section.items.map((item) => (_jsx(NavLink, { to: item.to, end: item.to === config.homePath, className: "admin-nav-item", children: ({ isActive }) => (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "material-symbols-outlined flex-shrink-0", style: { ...ICON_STYLE, fontVariationSettings: isActive ? "'FILL' 1" : "'FILL' 0" }, children: item.icon }), _jsx("span", { className: "font-sans", children: item.label })] })) }, item.to))) })] }, section.label))) }), _jsxs("div", { className: "px-3 pb-4 pt-4 space-y-0.5", style: { borderTop: '1px solid var(--m3-outline-variant)' }, children: [config.externalLinks.map((link) => (_jsxs("a", { href: link.href(), target: "_blank", rel: "noopener", className: "admin-nav-item", children: [_jsx("span", { "aria-hidden": "true", className: "material-symbols-outlined flex-shrink-0", style: ICON_STYLE, children: link.icon }), _jsx("span", { className: "font-sans", children: link.label })] }, link.label))), _jsxs(Link, { to: config.settingsPath, className: "admin-nav-item", children: [_jsx("span", { "aria-hidden": "true", className: "material-symbols-outlined flex-shrink-0", style: ICON_STYLE, children: "manage_accounts" }), _jsx("span", { className: "font-sans", children: "Settings" })] }), _jsxs("button", { type: "button", onClick: onToggleDark, className: "admin-nav-item", children: [_jsx("span", { "aria-hidden": "true", className: "material-symbols-outlined flex-shrink-0", style: ICON_STYLE, children: isDark ? 'light_mode' : 'dark_mode' }), _jsx("span", { className: "font-sans", children: isDark ? 'Light Mode' : 'Dark Mode' })] }), _jsxs("button", { type: "button", onClick: onLogout, className: "admin-nav-item admin-nav-danger", children: [_jsx("span", { "aria-hidden": "true", className: "material-symbols-outlined flex-shrink-0", style: ICON_STYLE, children: "logout" }), _jsx("span", { className: "font-sans", children: "Logout" })] })] })] }));
16
+ }
17
+ function MobileDrawer({ onClose, returnFocusRef, ...nav }) {
18
+ const closeRef = useRef(null);
19
+ useEffect(() => {
20
+ closeRef.current?.focus();
21
+ const returnTo = returnFocusRef.current;
22
+ const onKeyDown = (event) => { if (event.key === 'Escape')
23
+ onClose(); };
24
+ document.addEventListener('keydown', onKeyDown);
25
+ return () => {
26
+ document.removeEventListener('keydown', onKeyDown);
27
+ returnTo?.focus();
28
+ };
29
+ }, [onClose, returnFocusRef]);
30
+ return (_jsxs("div", { className: "md:hidden fixed inset-0 z-50", children: [_jsx("div", { "data-testid": "nav-drawer-overlay", className: "admin-drawer-overlay absolute inset-0", style: { background: 'rgba(0,0,0,0.55)' }, onClick: onClose }), _jsxs("div", { id: "admin-nav-drawer", role: "dialog", "aria-modal": "true", "aria-label": "Navigation", className: "admin-drawer-panel absolute inset-y-0 left-0 flex flex-col w-72 max-w-[85vw]", style: { background: 'var(--m3-surface-container-low)', boxShadow: '0 8px 32px rgba(0,0,0,0.18)' }, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-4 pt-4 mb-4", children: [_jsx(Brand, { brand: nav.config.brand }), _jsx("button", { ref: closeRef, type: "button", onClick: onClose, "aria-label": "Close navigation", className: "btn-icon", children: _jsx("span", { "aria-hidden": "true", className: "material-symbols-outlined", children: "close" }) })] }), _jsx(NavContent, { ...nav })] })] }));
31
+ }
32
+ /**
33
+ * The signed-in shell of an admin interface: a sidebar (a drawer on a phone) with the sections the user's role may see,
34
+ * the footer links, the theme switch and logout, and the page below it. The sections, roles, brand and links are the
35
+ * application's; the page is whatever the router renders in the `Outlet`.
36
+ */
37
+ export function createAdminShell(config) {
38
+ return function AdminShell() {
39
+ const navigate = useNavigate();
40
+ const location = useLocation();
41
+ const [isDark, toggleDark] = useDarkMode();
42
+ const datePrefs = useDatePrefs();
43
+ const currentUser = getCurrentUser();
44
+ const userRank = config.roleRank[currentUser?.role ?? ''] ?? 0;
45
+ // The drawer remembers the path it was opened on, so navigating anywhere closes it without an effect.
46
+ const [drawerPath, setDrawerPath] = useState(null);
47
+ const drawerOpen = drawerPath === location.pathname;
48
+ const menuButtonRef = useRef(null);
49
+ const closeDrawer = useCallback(() => setDrawerPath(null), []);
50
+ const navSections = config.sections
51
+ .map((section) => ({
52
+ ...section,
53
+ items: section.items.filter((item) => userRank >= (config.roleRank[item.minRole] ?? 99)),
54
+ }))
55
+ .filter((section) => section.items.length > 0);
56
+ const { changelog, version, docsUrl } = config.whatsNew;
57
+ const userId = currentUser?.userId;
58
+ const [whatsNew, setWhatsNew] = useState(() => userId !== undefined ? pendingWhatsNew(userId, changelog, version) : []);
59
+ const closeWhatsNew = useCallback(() => {
60
+ setWhatsNew([]);
61
+ if (userId !== undefined)
62
+ markWhatsNewSeen(userId, version);
63
+ }, [userId, version]);
64
+ async function handleLogout() {
65
+ try {
66
+ await api.post('/auth/logout');
67
+ }
68
+ catch { /* local logout still proceeds */ }
69
+ clearSession();
70
+ navigate(config.loginPath);
71
+ }
72
+ const nav = { config, sections: navSections, isDark, onToggleDark: toggleDark, onLogout: () => void handleLogout() };
73
+ return (_jsxs("div", { className: "flex flex-col md:flex-row h-screen", style: { background: 'var(--m3-surface-container-low)' }, children: [_jsxs("header", { className: "md:hidden flex items-center justify-between gap-3 px-4 py-3 flex-shrink-0", style: { background: 'var(--m3-surface-container-low)', borderBottom: '1px solid var(--m3-outline-variant)' }, children: [_jsx(Brand, { brand: config.brand }), _jsx("button", { ref: menuButtonRef, type: "button", onClick: () => setDrawerPath(location.pathname), "aria-label": "Open navigation", "aria-expanded": drawerOpen, "aria-controls": "admin-nav-drawer", className: "btn-icon", children: _jsx("span", { "aria-hidden": "true", className: "material-symbols-outlined", children: "menu" }) })] }), whatsNew.length > 0 && _jsx(WhatsNew, { releases: whatsNew, docsUrl: docsUrl, onClose: closeWhatsNew }), drawerOpen && _jsx(MobileDrawer, { ...nav, onClose: closeDrawer, returnFocusRef: menuButtonRef }), _jsxs("aside", { className: "hidden md:flex flex-col h-screen w-64 sticky top-0 flex-shrink-0", style: { background: 'var(--m3-surface-container-low)' }, children: [_jsx("div", { className: "mb-6 px-4 pt-5", children: _jsx(Brand, { brand: config.brand }) }), _jsx(NavContent, { ...nav })] }), _jsx("main", { className: "flex-1 min-h-0 min-w-0 overflow-auto", style: { background: 'var(--m3-surface-container-low)' }, children: _jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(Fragment, { children: _jsx(Outlet, {}) }, `${datePrefs.date}-${datePrefs.time}`) }) })] }));
74
+ };
75
+ }
76
+ //# sourceMappingURL=AdminShell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdminShell.js","sourceRoot":"","sources":["../../src/shell/AdminShell.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from 'react';
2
+ /** Sends a first visit to the setup page, while the application has no administrator yet. */
3
+ export declare function SetupGate({ children }: {
4
+ children: ReactNode;
5
+ }): import("react").JSX.Element;
6
+ /**
7
+ * Renders its children for a signed-in user. Someone who is not signed in goes to the sign-in page; someone the
8
+ * application sends elsewhere (`redirectAfterSignIn`) leaves the interface for that address.
9
+ */
10
+ export declare function RequireAuth({ children }: {
11
+ children: ReactNode;
12
+ }): import("react").JSX.Element | null;
13
+ /** Keeps a user who has to replace a temporary password on the page where they do it. */
14
+ export declare function RequirePasswordChanged({ children }: {
15
+ children: ReactNode;
16
+ }): import("react").JSX.Element;
17
+ /**
18
+ * A guard for pages that need a role: a user whose rank is below the page's goes to the home page of their role.
19
+ * `roleRank` lists the roles by rank (higher sees more); a role it does not know ranks lowest.
20
+ */
21
+ export declare function createRoleGuard(options: {
22
+ roleRank: Readonly<Record<string, number>>;
23
+ homeOf: (role?: string) => string;
24
+ }): ({ minRole, children }: {
25
+ minRole: string;
26
+ children: ReactNode;
27
+ }) => import("react").JSX.Element;
28
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/shell/guards.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAM3D,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAiC9D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,sCAsBhE;AAED,yFAAyF;AACzF,wBAAgB,sBAAsB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAG3E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;CAAE,IAC5F,uBAAuB;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,iCAM5F"}
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Navigate } from 'react-router-dom';
4
+ import { api, clearSession, getCurrentUser, mustChangePassword, setSession } from '../api/client';
5
+ import { uiConfig } from '../config';
6
+ import { navigation } from '../navigation';
7
+ /** Sends a first visit to the setup page, while the application has no administrator yet. */
8
+ export function SetupGate({ children }) {
9
+ const [state, setState] = useState('loading');
10
+ useEffect(() => {
11
+ fetch(`${uiConfig().apiBase}/setup/status`)
12
+ .then((r) => r.json())
13
+ .then((data) => {
14
+ setState(data.needsSetup ? 'setup' : 'ready');
15
+ })
16
+ .catch(() => setState('ready'));
17
+ }, []);
18
+ if (state === 'loading') {
19
+ return (_jsx("div", { className: "min-h-screen flex items-center justify-center", style: { background: 'var(--m3-surface)' }, children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("div", { className: "w-10 h-10 rounded-2xl flex items-center justify-center", style: { background: 'var(--m3-primary-fixed)' }, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", children: [_jsx("circle", { cx: "10", cy: "10", r: "3.5", fill: "var(--m3-primary)" }), _jsx("circle", { cx: "10", cy: "10", r: "8.5", stroke: "var(--m3-primary)", strokeWidth: "1.2", strokeOpacity: "0.35", fill: "none" })] }) }), _jsx("span", { className: "font-sans text-sm", style: { color: 'var(--m3-secondary)' }, children: "Starting\u2026" })] }) }));
20
+ }
21
+ if (state === 'setup')
22
+ return _jsx(Navigate, { to: `${uiConfig().basePath}/setup`, replace: true });
23
+ return _jsx(_Fragment, { children: children });
24
+ }
25
+ /**
26
+ * Renders its children for a signed-in user. Someone who is not signed in goes to the sign-in page; someone the
27
+ * application sends elsewhere (`redirectAfterSignIn`) leaves the interface for that address.
28
+ */
29
+ export function RequireAuth({ children }) {
30
+ const [state, setState] = useState('checking');
31
+ const [elsewhere, setElsewhere] = useState(null);
32
+ useEffect(() => {
33
+ api.get('/auth/session')
34
+ .then((user) => {
35
+ setSession(user);
36
+ const target = uiConfig().redirectAfterSignIn?.(user) ?? null;
37
+ setElsewhere(target);
38
+ setState(target ? 'elsewhere' : 'authenticated');
39
+ })
40
+ .catch(() => { clearSession(); setState('anonymous'); });
41
+ }, []);
42
+ useEffect(() => {
43
+ if (state === 'elsewhere' && elsewhere)
44
+ navigation.assign(elsewhere);
45
+ }, [state, elsewhere]);
46
+ if (state === 'checking' || state === 'elsewhere')
47
+ return null;
48
+ if (state === 'anonymous')
49
+ return _jsx(Navigate, { to: `${uiConfig().basePath}/login`, replace: true });
50
+ return _jsx(_Fragment, { children: children });
51
+ }
52
+ /** Keeps a user who has to replace a temporary password on the page where they do it. */
53
+ export function RequirePasswordChanged({ children }) {
54
+ if (mustChangePassword())
55
+ return _jsx(Navigate, { to: `${uiConfig().basePath}/change-password`, replace: true });
56
+ return _jsx(_Fragment, { children: children });
57
+ }
58
+ /**
59
+ * A guard for pages that need a role: a user whose rank is below the page's goes to the home page of their role.
60
+ * `roleRank` lists the roles by rank (higher sees more); a role it does not know ranks lowest.
61
+ */
62
+ export function createRoleGuard(options) {
63
+ return function RequireRole({ minRole, children }) {
64
+ const user = getCurrentUser();
65
+ const rank = options.roleRank[user?.role ?? ''] ?? 0;
66
+ if (rank < (options.roleRank[minRole] ?? 99))
67
+ return _jsx(Navigate, { to: options.homeOf(user?.role), replace: true });
68
+ return _jsx(_Fragment, { children: children });
69
+ };
70
+ }
71
+ //# sourceMappingURL=guards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/shell/guards.tsx"],"names":[],"mappings":"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"}
package/icons.json ADDED
@@ -0,0 +1,170 @@
1
+ [
2
+ "accessible",
3
+ "add",
4
+ "add_link",
5
+ "animation",
6
+ "api",
7
+ "archive",
8
+ "article",
9
+ "backup",
10
+ "badge",
11
+ "block",
12
+ "box",
13
+ "call",
14
+ "cancel",
15
+ "celebration",
16
+ "check",
17
+ "check_circle",
18
+ "circle",
19
+ "class",
20
+ "clear",
21
+ "close",
22
+ "cloud",
23
+ "cloud_off",
24
+ "code",
25
+ "colors",
26
+ "compare",
27
+ "content_copy",
28
+ "cookie",
29
+ "create",
30
+ "cut",
31
+ "dark_mode",
32
+ "data_object",
33
+ "database",
34
+ "delete",
35
+ "description",
36
+ "details",
37
+ "dialogs",
38
+ "docs",
39
+ "done",
40
+ "download",
41
+ "dropdown",
42
+ "email",
43
+ "enable",
44
+ "error",
45
+ "event",
46
+ "expand",
47
+ "expand_more",
48
+ "explicit",
49
+ "feedback",
50
+ "files",
51
+ "filter",
52
+ "function",
53
+ "gradient",
54
+ "group",
55
+ "groups",
56
+ "height",
57
+ "help",
58
+ "hide",
59
+ "history",
60
+ "home",
61
+ "host",
62
+ "html",
63
+ "http",
64
+ "https",
65
+ "inbox",
66
+ "info",
67
+ "input",
68
+ "join",
69
+ "keep",
70
+ "key",
71
+ "key_off",
72
+ "label",
73
+ "light_mode",
74
+ "link",
75
+ "list",
76
+ "lists",
77
+ "lock",
78
+ "lock_reset",
79
+ "login",
80
+ "logout",
81
+ "mail",
82
+ "manage_accounts",
83
+ "map",
84
+ "markdown",
85
+ "menu",
86
+ "message",
87
+ "mobile",
88
+ "mode",
89
+ "more",
90
+ "moving",
91
+ "navigation",
92
+ "network_check",
93
+ "no_encryption",
94
+ "note",
95
+ "notes",
96
+ "opacity",
97
+ "pace",
98
+ "package",
99
+ "padding",
100
+ "pages",
101
+ "passkey",
102
+ "password",
103
+ "pending",
104
+ "person",
105
+ "person_add",
106
+ "phone",
107
+ "place",
108
+ "post",
109
+ "progress_activity",
110
+ "public",
111
+ "radio_button_checked",
112
+ "refresh",
113
+ "remove",
114
+ "repeat",
115
+ "resize",
116
+ "restore",
117
+ "router",
118
+ "save",
119
+ "scan",
120
+ "schedule",
121
+ "script",
122
+ "search",
123
+ "security",
124
+ "select",
125
+ "send",
126
+ "settings",
127
+ "shadow",
128
+ "share",
129
+ "shield_lock",
130
+ "shift",
131
+ "sort",
132
+ "source",
133
+ "stack",
134
+ "start",
135
+ "step",
136
+ "steps",
137
+ "stop",
138
+ "storage",
139
+ "store",
140
+ "style",
141
+ "summarize",
142
+ "switch",
143
+ "tab",
144
+ "table",
145
+ "tabs",
146
+ "target",
147
+ "timer",
148
+ "title",
149
+ "toast",
150
+ "token",
151
+ "toolbar",
152
+ "transform",
153
+ "translate",
154
+ "trip",
155
+ "try",
156
+ "tune",
157
+ "unfold_more",
158
+ "update",
159
+ "upload",
160
+ "uppercase",
161
+ "variables",
162
+ "verified",
163
+ "visibility",
164
+ "visibility_off",
165
+ "vpn_key",
166
+ "warning",
167
+ "width",
168
+ "window",
169
+ "work"
170
+ ]
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
- {
2
- "name": "@better-forge/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@better-forge/ui",
3
+ "version": "0.1.0",
4
+ "description": "React admin shell, UI components, API client and shared pages.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/BElluu/Better-Forge.git",
9
+ "directory": "packages/ui"
10
+ },
11
+ "homepage": "https://github.com/BElluu/Better-Forge/tree/main/packages/ui#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/BElluu/Better-Forge/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./styles.css": "./styles/base.css",
27
+ "./icons.css": "./styles/icons.css",
28
+ "./icons.json": "./icons.json",
29
+ "./tailwind-preset": "./tailwind-preset.js",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "!src/**/*.test.*",
36
+ "!src/testing",
37
+ "styles",
38
+ "tailwind-preset.js",
39
+ "icons.json"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
44
+ },
45
+ "peerDependencies": {
46
+ "react": "^19.0.0",
47
+ "react-dom": "^19.0.0",
48
+ "clsx": "^2.1.1",
49
+ "react-router-dom": "^7.5.0",
50
+ "@tanstack/react-query": "^5.74.4"
51
+ },
52
+ "devDependencies": {
53
+ "typescript": "^5.7.3",
54
+ "@types/react": "^19.0.10",
55
+ "@types/react-dom": "^19.0.4",
56
+ "clsx": "^2.1.1",
57
+ "react": "^19.0.0",
58
+ "react-dom": "^19.0.0",
59
+ "react-router-dom": "^7.5.0",
60
+ "@tanstack/react-query": "^5.74.4"
61
+ },
62
+ "sideEffects": [
63
+ "*.css"
64
+ ],
65
+ "dependencies": {
66
+ "@better-forge/contracts": "0.1.0"
67
+ },
68
+ "engines": {
69
+ "node": ">=26.10.0"
70
+ }
71
+ }
@@ -0,0 +1,139 @@
1
+ import { safeSessionStorage } from '@better-forge/contracts'
2
+ import { storageKey, uiConfig } from '../config'
3
+ import { navigation } from '../navigation'
4
+
5
+ const userKey = () => storageKey('auth-user')
6
+
7
+ export interface AuthUser {
8
+ userId: number
9
+ email: string
10
+ role: string
11
+ mustChangePassword: boolean
12
+ twoFactorEnabled: boolean
13
+ authMethod?: 'password' | 'oidc'
14
+ }
15
+
16
+ export function setSession(user: AuthUser): void {
17
+ safeSessionStorage.set(userKey(), JSON.stringify(user))
18
+ window.dispatchEvent(new CustomEvent(storageKey('auth-change')))
19
+ }
20
+
21
+ export function clearSession(): void {
22
+ safeSessionStorage.remove(userKey())
23
+ window.dispatchEvent(new CustomEvent(storageKey('auth-change')))
24
+ }
25
+
26
+ export function getCurrentUser(): AuthUser | null {
27
+ try {
28
+ const stored = safeSessionStorage.get(userKey())
29
+ return stored ? JSON.parse(stored) as AuthUser : null
30
+ } catch { return null }
31
+ }
32
+
33
+ export function isAuthenticated(): boolean {
34
+ return getCurrentUser() !== null
35
+ }
36
+
37
+ /**
38
+ * Where a signed-in user starts. One who does not belong in this interface goes where the application sends them
39
+ * (which may also have them replace a temporary password); everyone else replaces it here first.
40
+ */
41
+ export function enterAfterSignIn(user: AuthUser, navigate: (path: string) => void): void {
42
+ const elsewhere = uiConfig().redirectAfterSignIn?.(user) ?? null
43
+ if (elsewhere) navigation.assign(elsewhere)
44
+ else if (user.mustChangePassword) navigate(`${uiConfig().basePath}/change-password`)
45
+ else navigate(`${uiConfig().basePath}/`)
46
+ }
47
+
48
+ export function mustChangePassword(): boolean {
49
+ return !!getCurrentUser()?.mustChangePassword
50
+ }
51
+
52
+ /**
53
+ * A failed API call: `message` is the server's error text, `code` its machine-readable code when it sent one, and
54
+ * `body` the whole JSON answer, for the endpoints that say more than a message (a list of problems, say).
55
+ */
56
+ export class ApiError extends Error {
57
+ constructor(message: string, readonly status: number, readonly code?: string, readonly body?: unknown) {
58
+ super(message)
59
+ this.name = 'ApiError'
60
+ }
61
+ }
62
+
63
+ function cookie(name: string): string | null {
64
+ const prefix = `${encodeURIComponent(name)}=`
65
+ const part = document.cookie.split('; ').find((item) => item.startsWith(prefix))
66
+ return part ? decodeURIComponent(part.slice(prefix.length)) : null
67
+ }
68
+
69
+ async function request<T>(
70
+ method: string,
71
+ path: string,
72
+ body?: unknown,
73
+ isFormData = false,
74
+ /** Sends `body` (a string) as it is with this content type, instead of as JSON. */
75
+ contentType?: string,
76
+ /** Reads the answer as text instead of JSON. */
77
+ asText = false,
78
+ ): Promise<T> {
79
+ const headers: Record<string, string> = {}
80
+ if (contentType) headers['Content-Type'] = contentType
81
+ else if (!isFormData && body !== undefined) headers['Content-Type'] = 'application/json'
82
+ if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
83
+ const csrf = cookie(uiConfig().csrfCookie)
84
+ if (csrf) headers['X-CSRF-Token'] = csrf
85
+ }
86
+
87
+ const fetchBody = contentType ? (body as string) : isFormData ? (body as FormData) : body !== undefined ? JSON.stringify(body) : null
88
+ const res = await fetch(`${uiConfig().apiBase}${path}`, {
89
+ method,
90
+ headers,
91
+ credentials: 'same-origin',
92
+ ...(fetchBody !== null ? { body: fetchBody } : {}),
93
+ })
94
+
95
+ if (!res.ok) {
96
+ const err = await res.json().catch(() => ({ error: res.statusText })) as { error?: string; code?: string }
97
+ if (res.status === 403 && err.code === 'PASSWORD_CHANGE_REQUIRED') {
98
+ const user = getCurrentUser()
99
+ if (user && !user.mustChangePassword) setSession({ ...user, mustChangePassword: true })
100
+ if (!window.location.pathname.includes('/change-password')) {
101
+ window.location.href = `${uiConfig().basePath}/change-password`
102
+ }
103
+ }
104
+ if (res.status === 401) {
105
+ clearSession()
106
+ const isLoginRequest = path === '/auth/login' || path === '/auth/2fa/verify'
107
+ if (!isLoginRequest && !window.location.pathname.includes('/login')) {
108
+ window.location.href = `${uiConfig().basePath}/login`
109
+ }
110
+ }
111
+ throw new ApiError(err.error ?? res.statusText, res.status, err.code, err)
112
+ }
113
+
114
+ if (res.status === 204) return undefined as T
115
+ return (asText ? res.text() : res.json()) as Promise<T>
116
+ }
117
+
118
+ export const api = {
119
+ get: <T>(path: string) => request<T>('GET', path),
120
+ /** GET an answer that is text, for example a YAML document. */
121
+ getText: (path: string) => request<string>('GET', path, undefined, false, undefined, true),
122
+ post: <T>(path: string, body: unknown = {}) => request<T>('POST', path, body),
123
+ patch: <T>(path: string, body: unknown) => request<T>('PATCH', path, body),
124
+ put: <T>(path: string, body: unknown) => request<T>('PUT', path, body),
125
+ delete: (path: string) => request<void>('DELETE', path),
126
+ upload: <T>(path: string, formData: FormData) => request<T>('POST', path, formData, true),
127
+ /** POST a text body as it is, for example a YAML file, with the given content type. */
128
+ postText: <T>(path: string, text: string, contentType: string) => request<T>('POST', path, text, false, contentType),
129
+ download: async (path: string, filename: string) => {
130
+ const res = await fetch(`${uiConfig().apiBase}${path}`, { credentials: 'same-origin' })
131
+ if (!res.ok) throw new Error('Download failed')
132
+ const url = URL.createObjectURL(await res.blob())
133
+ const link = document.createElement('a')
134
+ link.href = url
135
+ link.download = filename
136
+ link.click()
137
+ URL.revokeObjectURL(url)
138
+ },
139
+ }