@better-forge/ui 0.1.0 → 0.1.1
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/LICENSE +21 -21
- package/README.md +13 -13
- package/icons.json +170 -170
- package/package.json +71 -71
- package/src/api/client.ts +139 -139
- package/src/components/ConfirmIdentityModal.tsx +82 -82
- package/src/components/ConfirmModal.tsx +83 -83
- package/src/components/CopyButton.tsx +71 -71
- package/src/components/CredentialSection.tsx +270 -270
- package/src/components/ErrorBoundary.tsx +47 -47
- package/src/components/ModalShell.tsx +160 -160
- package/src/components/ResetTwoFactorModal.tsx +74 -74
- package/src/components/SidePanel.tsx +96 -96
- package/src/components/SingleSignOnModal.tsx +161 -161
- package/src/components/SsoConfirmation.tsx +96 -96
- package/src/components/VaultTypePicker.tsx +131 -131
- package/src/components/WhatsNew.tsx +84 -84
- package/src/components/formParts.tsx +59 -59
- package/src/components/ui/Alert.tsx +45 -45
- package/src/components/ui/DataTable.tsx +225 -225
- package/src/components/ui/Field.tsx +63 -63
- package/src/components/ui/PageHeader.tsx +36 -36
- package/src/components/ui/Pagination.tsx +37 -37
- package/src/components/ui/QuickStartPanel.tsx +57 -57
- package/src/components/ui/StateViews.tsx +135 -135
- package/src/components/ui/Switch.tsx +65 -65
- package/src/components/ui/Toast.tsx +104 -104
- package/src/components/ui/index.ts +9 -9
- package/src/config.ts +46 -46
- package/src/hooks/useDarkMode.ts +38 -38
- package/src/hooks/useDebouncedValue.ts +22 -22
- package/src/index.ts +42 -42
- package/src/lib/changelog.ts +93 -93
- package/src/lib/dateFormat.ts +90 -90
- package/src/navigation.ts +6 -6
- package/src/pages/ApiTokens.tsx +361 -361
- package/src/pages/AuditLog.tsx +388 -388
- package/src/pages/Backups.tsx +170 -170
- package/src/pages/ChangePassword.tsx +84 -84
- package/src/pages/Login.tsx +275 -275
- package/src/pages/Settings.tsx +264 -264
- package/src/pages/Setup.tsx +438 -438
- package/src/pages/SsoConfirm.tsx +26 -26
- package/src/pages/SsoTestResult.tsx +104 -104
- package/src/pages/Users.tsx +426 -426
- package/src/pages/Vault.tsx +1036 -1036
- package/src/shell/AdminShell.tsx +256 -256
- package/src/shell/guards.tsx +88 -88
- package/styles/base.css +521 -521
- package/styles/icons.css +4 -4
- package/tailwind-preset.js +79 -79
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import type { ReactNode } from 'react'
|
|
2
|
-
|
|
3
|
-
// Building blocks shared by the forms of the interface and the sections extracted from them.
|
|
4
|
-
|
|
5
|
-
export interface VaultSummary { id: number; name: string }
|
|
6
|
-
export interface SecretSummary { id: number; name: string; type: 'userpass' | 'value' | 'json' }
|
|
7
|
-
|
|
8
|
-
/** What every vault-aware credential picker needs to list vaults and lazily load their secrets. */
|
|
9
|
-
export interface VaultPickerProps {
|
|
10
|
-
vaults: { id: number; name: string }[]
|
|
11
|
-
secretsByVault: Record<number, { id: number; name: string; type: string }[]>
|
|
12
|
-
onLoadSecrets: (vaultId: number) => Promise<void>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Fields that can be sourced from a json vault secret, per context
|
|
16
|
-
export const JSON_MAPPING_FIELDS: Record<'basic' | 'oauth2' | 'cas' | 'database', { key: string; label: string }[]> = {
|
|
17
|
-
basic: [{ key: 'username', label: 'Username' }, { key: 'password', label: 'Password' }],
|
|
18
|
-
oauth2: [{ key: 'clientId', label: 'Client ID' }, { key: 'clientSecret', label: 'Client Secret' }],
|
|
19
|
-
cas: [{ key: 'username', label: 'Username' }, { key: 'password', label: 'Password' }],
|
|
20
|
-
database: [{ key: 'username', label: 'Username' }, { key: 'password', label: 'Password' }],
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** The shared caps-style form field: its label is programmatically tied to the control it wraps. */
|
|
24
|
-
export { Field } from './ui'
|
|
25
|
-
|
|
26
|
-
export function SectionDivider({ label }: { label: string }) {
|
|
27
|
-
return (
|
|
28
|
-
<div className="flex items-center gap-3 pt-1">
|
|
29
|
-
<div style={{ flex: 1, borderTop: '1px solid var(--m3-outline-variant)' }} />
|
|
30
|
-
<span className="font-mono text-xs uppercase tracking-wider" style={{ color: 'var(--m3-secondary)' }}>{label}</span>
|
|
31
|
-
<div style={{ flex: 1, borderTop: '1px solid var(--m3-outline-variant)' }} />
|
|
32
|
-
</div>
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Tinted callout: `info` uses the accent blue, `warning` the degraded amber (text colour, not the bar colour). */
|
|
37
|
-
export function Note({ tone, children }: { tone: 'info' | 'warning'; children: ReactNode }) {
|
|
38
|
-
const color = tone === 'info' ? 'var(--m3-on-primary-container)' : 'var(--m3-degraded)'
|
|
39
|
-
const tint = tone === 'info' ? color : 'var(--m3-degraded-bar)'
|
|
40
|
-
return (
|
|
41
|
-
<div
|
|
42
|
-
className="flex items-start gap-2 rounded-lg px-3 py-2"
|
|
43
|
-
style={{ background: `color-mix(in srgb, ${tint} 8%, transparent)`, border: `1px solid color-mix(in srgb, ${tint} 20%, transparent)` }}
|
|
44
|
-
>
|
|
45
|
-
{tone === 'info'
|
|
46
|
-
? <span className="material-symbols-outlined" aria-hidden="true" style={{ fontSize: '16px', color, lineHeight: '20px' }}>info</span>
|
|
47
|
-
: <span aria-hidden="true" style={{ color, lineHeight: '20px' }}>⚠</span>}
|
|
48
|
-
<p className="text-xs" style={{ color: 'var(--m3-on-surface-variant)' }}>{children}</p>
|
|
49
|
-
</div>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Adds or removes one id from a selection set without mutating it. */
|
|
54
|
-
export function toggleInSet(set: Set<number>, id: number, on: boolean): Set<number> {
|
|
55
|
-
const next = new Set(set)
|
|
56
|
-
if (on) next.add(id)
|
|
57
|
-
else next.delete(id)
|
|
58
|
-
return next
|
|
59
|
-
}
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
// Building blocks shared by the forms of the interface and the sections extracted from them.
|
|
4
|
+
|
|
5
|
+
export interface VaultSummary { id: number; name: string }
|
|
6
|
+
export interface SecretSummary { id: number; name: string; type: 'userpass' | 'value' | 'json' }
|
|
7
|
+
|
|
8
|
+
/** What every vault-aware credential picker needs to list vaults and lazily load their secrets. */
|
|
9
|
+
export interface VaultPickerProps {
|
|
10
|
+
vaults: { id: number; name: string }[]
|
|
11
|
+
secretsByVault: Record<number, { id: number; name: string; type: string }[]>
|
|
12
|
+
onLoadSecrets: (vaultId: number) => Promise<void>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Fields that can be sourced from a json vault secret, per context
|
|
16
|
+
export const JSON_MAPPING_FIELDS: Record<'basic' | 'oauth2' | 'cas' | 'database', { key: string; label: string }[]> = {
|
|
17
|
+
basic: [{ key: 'username', label: 'Username' }, { key: 'password', label: 'Password' }],
|
|
18
|
+
oauth2: [{ key: 'clientId', label: 'Client ID' }, { key: 'clientSecret', label: 'Client Secret' }],
|
|
19
|
+
cas: [{ key: 'username', label: 'Username' }, { key: 'password', label: 'Password' }],
|
|
20
|
+
database: [{ key: 'username', label: 'Username' }, { key: 'password', label: 'Password' }],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The shared caps-style form field: its label is programmatically tied to the control it wraps. */
|
|
24
|
+
export { Field } from './ui'
|
|
25
|
+
|
|
26
|
+
export function SectionDivider({ label }: { label: string }) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex items-center gap-3 pt-1">
|
|
29
|
+
<div style={{ flex: 1, borderTop: '1px solid var(--m3-outline-variant)' }} />
|
|
30
|
+
<span className="font-mono text-xs uppercase tracking-wider" style={{ color: 'var(--m3-secondary)' }}>{label}</span>
|
|
31
|
+
<div style={{ flex: 1, borderTop: '1px solid var(--m3-outline-variant)' }} />
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Tinted callout: `info` uses the accent blue, `warning` the degraded amber (text colour, not the bar colour). */
|
|
37
|
+
export function Note({ tone, children }: { tone: 'info' | 'warning'; children: ReactNode }) {
|
|
38
|
+
const color = tone === 'info' ? 'var(--m3-on-primary-container)' : 'var(--m3-degraded)'
|
|
39
|
+
const tint = tone === 'info' ? color : 'var(--m3-degraded-bar)'
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className="flex items-start gap-2 rounded-lg px-3 py-2"
|
|
43
|
+
style={{ background: `color-mix(in srgb, ${tint} 8%, transparent)`, border: `1px solid color-mix(in srgb, ${tint} 20%, transparent)` }}
|
|
44
|
+
>
|
|
45
|
+
{tone === 'info'
|
|
46
|
+
? <span className="material-symbols-outlined" aria-hidden="true" style={{ fontSize: '16px', color, lineHeight: '20px' }}>info</span>
|
|
47
|
+
: <span aria-hidden="true" style={{ color, lineHeight: '20px' }}>⚠</span>}
|
|
48
|
+
<p className="text-xs" style={{ color: 'var(--m3-on-surface-variant)' }}>{children}</p>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Adds or removes one id from a selection set without mutating it. */
|
|
54
|
+
export function toggleInSet(set: Set<number>, id: number, on: boolean): Set<number> {
|
|
55
|
+
const next = new Set(set)
|
|
56
|
+
if (on) next.add(id)
|
|
57
|
+
else next.delete(id)
|
|
58
|
+
return next
|
|
59
|
+
}
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
|
-
import type { CSSProperties, ReactNode } from 'react'
|
|
3
|
-
|
|
4
|
-
export type AlertTone = 'error' | 'success' | 'info' | 'warning'
|
|
5
|
-
|
|
6
|
-
const TONES: Record<AlertTone, { icon: string; style: CSSProperties }> = {
|
|
7
|
-
error: { icon: 'error', style: { background: 'var(--m3-down-bg)', color: 'var(--m3-down)', borderColor: 'color-mix(in srgb, var(--m3-down) 30%, transparent)' } },
|
|
8
|
-
success: { icon: 'check_circle', style: { background: 'var(--m3-up-bg)', color: 'var(--m3-up)', borderColor: 'color-mix(in srgb, var(--m3-up) 30%, transparent)' } },
|
|
9
|
-
warning: { icon: 'warning', style: { background: 'var(--m3-degraded-bg)', color: 'var(--m3-degraded)', borderColor: 'color-mix(in srgb, var(--m3-degraded) 30%, transparent)' } },
|
|
10
|
-
info: { icon: 'info', style: { background: 'var(--m3-surface-container-high)', color: 'var(--m3-on-surface)', borderColor: 'var(--m3-outline-variant)' } },
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface AlertProps {
|
|
14
|
-
tone: AlertTone
|
|
15
|
-
children: ReactNode
|
|
16
|
-
/** Optional bold first line. */
|
|
17
|
-
title?: ReactNode | undefined
|
|
18
|
-
/** Renders a × button (aria-label "Dismiss") that calls this. */
|
|
19
|
-
onDismiss?: (() => void) | undefined
|
|
20
|
-
className?: string | undefined
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Inline, token-coloured message box. Errors are announced assertively (role="alert"); other tones politely. */
|
|
24
|
-
export function Alert({ tone, children, title, onDismiss, className }: AlertProps) {
|
|
25
|
-
const { icon, style } = TONES[tone]
|
|
26
|
-
return (
|
|
27
|
-
<div
|
|
28
|
-
role={tone === 'error' ? 'alert' : 'status'}
|
|
29
|
-
data-tone={tone}
|
|
30
|
-
className={clsx('flex items-start gap-3 rounded-xl border px-4 py-3 text-sm', className)}
|
|
31
|
-
style={style}
|
|
32
|
-
>
|
|
33
|
-
<span className="material-symbols-outlined flex-shrink-0 mt-px" aria-hidden="true" style={{ fontSize: '18px' }}>{icon}</span>
|
|
34
|
-
<div className="flex-1 min-w-0 break-words">
|
|
35
|
-
{title && <p className="font-semibold">{title}</p>}
|
|
36
|
-
<div className={title ? 'mt-0.5' : undefined}>{children}</div>
|
|
37
|
-
</div>
|
|
38
|
-
{onDismiss && (
|
|
39
|
-
<button type="button" onClick={onDismiss} aria-label="Dismiss" className="btn-icon -my-1 -mr-2" style={{ color: 'inherit' }}>
|
|
40
|
-
<span className="material-symbols-outlined" aria-hidden="true" style={{ fontSize: '18px' }}>close</span>
|
|
41
|
-
</button>
|
|
42
|
-
)}
|
|
43
|
-
</div>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
1
|
+
import clsx from 'clsx'
|
|
2
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
export type AlertTone = 'error' | 'success' | 'info' | 'warning'
|
|
5
|
+
|
|
6
|
+
const TONES: Record<AlertTone, { icon: string; style: CSSProperties }> = {
|
|
7
|
+
error: { icon: 'error', style: { background: 'var(--m3-down-bg)', color: 'var(--m3-down)', borderColor: 'color-mix(in srgb, var(--m3-down) 30%, transparent)' } },
|
|
8
|
+
success: { icon: 'check_circle', style: { background: 'var(--m3-up-bg)', color: 'var(--m3-up)', borderColor: 'color-mix(in srgb, var(--m3-up) 30%, transparent)' } },
|
|
9
|
+
warning: { icon: 'warning', style: { background: 'var(--m3-degraded-bg)', color: 'var(--m3-degraded)', borderColor: 'color-mix(in srgb, var(--m3-degraded) 30%, transparent)' } },
|
|
10
|
+
info: { icon: 'info', style: { background: 'var(--m3-surface-container-high)', color: 'var(--m3-on-surface)', borderColor: 'var(--m3-outline-variant)' } },
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AlertProps {
|
|
14
|
+
tone: AlertTone
|
|
15
|
+
children: ReactNode
|
|
16
|
+
/** Optional bold first line. */
|
|
17
|
+
title?: ReactNode | undefined
|
|
18
|
+
/** Renders a × button (aria-label "Dismiss") that calls this. */
|
|
19
|
+
onDismiss?: (() => void) | undefined
|
|
20
|
+
className?: string | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Inline, token-coloured message box. Errors are announced assertively (role="alert"); other tones politely. */
|
|
24
|
+
export function Alert({ tone, children, title, onDismiss, className }: AlertProps) {
|
|
25
|
+
const { icon, style } = TONES[tone]
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
role={tone === 'error' ? 'alert' : 'status'}
|
|
29
|
+
data-tone={tone}
|
|
30
|
+
className={clsx('flex items-start gap-3 rounded-xl border px-4 py-3 text-sm', className)}
|
|
31
|
+
style={style}
|
|
32
|
+
>
|
|
33
|
+
<span className="material-symbols-outlined flex-shrink-0 mt-px" aria-hidden="true" style={{ fontSize: '18px' }}>{icon}</span>
|
|
34
|
+
<div className="flex-1 min-w-0 break-words">
|
|
35
|
+
{title && <p className="font-semibold">{title}</p>}
|
|
36
|
+
<div className={title ? 'mt-0.5' : undefined}>{children}</div>
|
|
37
|
+
</div>
|
|
38
|
+
{onDismiss && (
|
|
39
|
+
<button type="button" onClick={onDismiss} aria-label="Dismiss" className="btn-icon -my-1 -mr-2" style={{ color: 'inherit' }}>
|
|
40
|
+
<span className="material-symbols-outlined" aria-hidden="true" style={{ fontSize: '18px' }}>close</span>
|
|
41
|
+
</button>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|