@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.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +13 -13
  3. package/icons.json +170 -170
  4. package/package.json +71 -71
  5. package/src/api/client.ts +139 -139
  6. package/src/components/ConfirmIdentityModal.tsx +82 -82
  7. package/src/components/ConfirmModal.tsx +83 -83
  8. package/src/components/CopyButton.tsx +71 -71
  9. package/src/components/CredentialSection.tsx +270 -270
  10. package/src/components/ErrorBoundary.tsx +47 -47
  11. package/src/components/ModalShell.tsx +160 -160
  12. package/src/components/ResetTwoFactorModal.tsx +74 -74
  13. package/src/components/SidePanel.tsx +96 -96
  14. package/src/components/SingleSignOnModal.tsx +161 -161
  15. package/src/components/SsoConfirmation.tsx +96 -96
  16. package/src/components/VaultTypePicker.tsx +131 -131
  17. package/src/components/WhatsNew.tsx +84 -84
  18. package/src/components/formParts.tsx +59 -59
  19. package/src/components/ui/Alert.tsx +45 -45
  20. package/src/components/ui/DataTable.tsx +225 -225
  21. package/src/components/ui/Field.tsx +63 -63
  22. package/src/components/ui/PageHeader.tsx +36 -36
  23. package/src/components/ui/Pagination.tsx +37 -37
  24. package/src/components/ui/QuickStartPanel.tsx +57 -57
  25. package/src/components/ui/StateViews.tsx +135 -135
  26. package/src/components/ui/Switch.tsx +65 -65
  27. package/src/components/ui/Toast.tsx +104 -104
  28. package/src/components/ui/index.ts +9 -9
  29. package/src/config.ts +46 -46
  30. package/src/hooks/useDarkMode.ts +38 -38
  31. package/src/hooks/useDebouncedValue.ts +22 -22
  32. package/src/index.ts +42 -42
  33. package/src/lib/changelog.ts +93 -93
  34. package/src/lib/dateFormat.ts +90 -90
  35. package/src/navigation.ts +6 -6
  36. package/src/pages/ApiTokens.tsx +361 -361
  37. package/src/pages/AuditLog.tsx +388 -388
  38. package/src/pages/Backups.tsx +170 -170
  39. package/src/pages/ChangePassword.tsx +84 -84
  40. package/src/pages/Login.tsx +275 -275
  41. package/src/pages/Settings.tsx +264 -264
  42. package/src/pages/Setup.tsx +438 -438
  43. package/src/pages/SsoConfirm.tsx +26 -26
  44. package/src/pages/SsoTestResult.tsx +104 -104
  45. package/src/pages/Users.tsx +426 -426
  46. package/src/pages/Vault.tsx +1036 -1036
  47. package/src/shell/AdminShell.tsx +256 -256
  48. package/src/shell/guards.tsx +88 -88
  49. package/styles/base.css +521 -521
  50. package/styles/icons.css +4 -4
  51. package/tailwind-preset.js +79 -79
@@ -1,93 +1,93 @@
1
- export interface ReleaseItem {
2
- title: string
3
- summary: string
4
- }
5
-
6
- export interface ReleaseGroup {
7
- heading: string
8
- items: ReleaseItem[]
9
- }
10
-
11
- export interface ReleaseNotes {
12
- version: string
13
- date: string
14
- groups: ReleaseGroup[]
15
- }
16
-
17
- const RELEASE_HEADING = /^## \[(\d+\.\d+\.\d+)\](?: - (\S+))?\s*$/
18
- const GROUP_HEADING = /^### (.+?)\s*$/
19
- const ITEM = /^- (?:\*\*(.+?)\*\*:?\s*)?(.*)$/
20
-
21
- function plain(text: string): string {
22
- return text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[`*]/g, '').trim()
23
- }
24
-
25
- function firstSentence(text: string): string {
26
- const match = /^(.+?[.!?])(?:\s|$)/.exec(text)
27
- return plain(match ? match[1]! : text)
28
- }
29
-
30
- /**
31
- * Released sections of docs/changelog.md, newest first; each top-level bullet is cut down to its bold title and
32
- * the first sentence of its text. The Unreleased section and nested bullets are left out.
33
- */
34
- export function parseChangelog(markdown: string): ReleaseNotes[] {
35
- const releases: ReleaseNotes[] = []
36
- let release: ReleaseNotes | null = null
37
- let group: ReleaseGroup | null = null
38
- for (const line of markdown.split(/\r?\n/)) {
39
- const releaseMatch = RELEASE_HEADING.exec(line)
40
- if (releaseMatch) {
41
- release = { version: releaseMatch[1]!, date: releaseMatch[2] ?? '', groups: [] }
42
- releases.push(release)
43
- group = null
44
- } else if (line.startsWith('## ')) {
45
- release = null
46
- } else if (release) {
47
- const groupMatch = GROUP_HEADING.exec(line)
48
- if (groupMatch) {
49
- group = { heading: groupMatch[1]!, items: [] }
50
- release.groups.push(group)
51
- continue
52
- }
53
- const item = group ? ITEM.exec(line) : null
54
- if (item && group) group.items.push({ title: plain(item[1] ?? ''), summary: firstSentence(item[2] ?? '') })
55
- }
56
- }
57
- return releases.map((r) => ({ ...r, groups: r.groups.filter((g) => g.items.length > 0) }))
58
- }
59
-
60
- /** Negative, zero or positive like a sort comparator; plain x.y.z versions only. */
61
- export function compareVersions(a: string, b: string): number {
62
- const left = a.split('.').map(Number)
63
- const right = b.split('.').map(Number)
64
- for (let i = 0; i < 3; i++) {
65
- const diff = (left[i] ?? 0) - (right[i] ?? 0)
66
- if (diff !== 0) return diff
67
- }
68
- return 0
69
- }
70
-
71
- /** Releases after `seen` up to and including `current`, newest first. */
72
- export function releasesBetween(releases: ReleaseNotes[], seen: string, current: string): ReleaseNotes[] {
73
- return releases.filter((r) => compareVersions(r.version, seen) > 0 && compareVersions(r.version, current) <= 0)
74
- }
75
-
76
- export interface ReleaseSummary {
77
- version: string
78
- added: ReleaseItem[]
79
- /** Changed, Removed and any other kind of entry that is not new or a fix. */
80
- changed: ReleaseItem[]
81
- fixed: ReleaseItem[]
82
- }
83
-
84
- /** One release sorted by the kind of change; fixes are only counted. */
85
- export function summarizeRelease(release: ReleaseNotes): ReleaseSummary {
86
- const summary: ReleaseSummary = { version: release.version, added: [], changed: [], fixed: [] }
87
- for (const group of release.groups) {
88
- if (group.heading === 'Added') summary.added.push(...group.items)
89
- else if (group.heading === 'Fixed') summary.fixed.push(...group.items)
90
- else summary.changed.push(...group.items)
91
- }
92
- return summary
93
- }
1
+ export interface ReleaseItem {
2
+ title: string
3
+ summary: string
4
+ }
5
+
6
+ export interface ReleaseGroup {
7
+ heading: string
8
+ items: ReleaseItem[]
9
+ }
10
+
11
+ export interface ReleaseNotes {
12
+ version: string
13
+ date: string
14
+ groups: ReleaseGroup[]
15
+ }
16
+
17
+ const RELEASE_HEADING = /^## \[(\d+\.\d+\.\d+)\](?: - (\S+))?\s*$/
18
+ const GROUP_HEADING = /^### (.+?)\s*$/
19
+ const ITEM = /^- (?:\*\*(.+?)\*\*:?\s*)?(.*)$/
20
+
21
+ function plain(text: string): string {
22
+ return text.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[`*]/g, '').trim()
23
+ }
24
+
25
+ function firstSentence(text: string): string {
26
+ const match = /^(.+?[.!?])(?:\s|$)/.exec(text)
27
+ return plain(match ? match[1]! : text)
28
+ }
29
+
30
+ /**
31
+ * Released sections of docs/changelog.md, newest first; each top-level bullet is cut down to its bold title and
32
+ * the first sentence of its text. The Unreleased section and nested bullets are left out.
33
+ */
34
+ export function parseChangelog(markdown: string): ReleaseNotes[] {
35
+ const releases: ReleaseNotes[] = []
36
+ let release: ReleaseNotes | null = null
37
+ let group: ReleaseGroup | null = null
38
+ for (const line of markdown.split(/\r?\n/)) {
39
+ const releaseMatch = RELEASE_HEADING.exec(line)
40
+ if (releaseMatch) {
41
+ release = { version: releaseMatch[1]!, date: releaseMatch[2] ?? '', groups: [] }
42
+ releases.push(release)
43
+ group = null
44
+ } else if (line.startsWith('## ')) {
45
+ release = null
46
+ } else if (release) {
47
+ const groupMatch = GROUP_HEADING.exec(line)
48
+ if (groupMatch) {
49
+ group = { heading: groupMatch[1]!, items: [] }
50
+ release.groups.push(group)
51
+ continue
52
+ }
53
+ const item = group ? ITEM.exec(line) : null
54
+ if (item && group) group.items.push({ title: plain(item[1] ?? ''), summary: firstSentence(item[2] ?? '') })
55
+ }
56
+ }
57
+ return releases.map((r) => ({ ...r, groups: r.groups.filter((g) => g.items.length > 0) }))
58
+ }
59
+
60
+ /** Negative, zero or positive like a sort comparator; plain x.y.z versions only. */
61
+ export function compareVersions(a: string, b: string): number {
62
+ const left = a.split('.').map(Number)
63
+ const right = b.split('.').map(Number)
64
+ for (let i = 0; i < 3; i++) {
65
+ const diff = (left[i] ?? 0) - (right[i] ?? 0)
66
+ if (diff !== 0) return diff
67
+ }
68
+ return 0
69
+ }
70
+
71
+ /** Releases after `seen` up to and including `current`, newest first. */
72
+ export function releasesBetween(releases: ReleaseNotes[], seen: string, current: string): ReleaseNotes[] {
73
+ return releases.filter((r) => compareVersions(r.version, seen) > 0 && compareVersions(r.version, current) <= 0)
74
+ }
75
+
76
+ export interface ReleaseSummary {
77
+ version: string
78
+ added: ReleaseItem[]
79
+ /** Changed, Removed and any other kind of entry that is not new or a fix. */
80
+ changed: ReleaseItem[]
81
+ fixed: ReleaseItem[]
82
+ }
83
+
84
+ /** One release sorted by the kind of change; fixes are only counted. */
85
+ export function summarizeRelease(release: ReleaseNotes): ReleaseSummary {
86
+ const summary: ReleaseSummary = { version: release.version, added: [], changed: [], fixed: [] }
87
+ for (const group of release.groups) {
88
+ if (group.heading === 'Added') summary.added.push(...group.items)
89
+ else if (group.heading === 'Fixed') summary.fixed.push(...group.items)
90
+ else summary.changed.push(...group.items)
91
+ }
92
+ return summary
93
+ }
@@ -1,90 +1,90 @@
1
- import { safeLocalStorage } from '@better-forge/contracts'
2
- import { useSyncExternalStore } from 'react'
3
- import { storageKey } from '../config'
4
-
5
- /** Admin date and time display, chosen per browser in Settings. */
6
- export type DateFormat = 'dmy' | 'dmyDash' | 'mdy' | 'ymd' | 'ymdDot'
7
- export type TimeFormat = '24h' | '12h'
8
- export interface DatePrefs { date: DateFormat; time: TimeFormat }
9
-
10
- interface DatePattern { label: string; full: (y: number, m: string, d: string) => string; dayMonth: (m: string, d: string) => string }
11
-
12
- const DATE_PATTERNS: Record<DateFormat, DatePattern> = {
13
- dmy: { label: 'dd.mm.yyyy', full: (y, m, d) => `${d}.${m}.${y}`, dayMonth: (m, d) => `${d}.${m}` },
14
- dmyDash: { label: 'dd-mm-yyyy', full: (y, m, d) => `${d}-${m}-${y}`, dayMonth: (m, d) => `${d}-${m}` },
15
- mdy: { label: 'mm/dd/yyyy', full: (y, m, d) => `${m}/${d}/${y}`, dayMonth: (m, d) => `${m}/${d}` },
16
- ymd: { label: 'yyyy-mm-dd', full: (y, m, d) => `${y}-${m}-${d}`, dayMonth: (m, d) => `${m}-${d}` },
17
- ymdDot: { label: 'yyyy.mm.dd', full: (y, m, d) => `${y}.${m}.${d}`, dayMonth: (m, d) => `${m}.${d}` },
18
- }
19
-
20
- export const DATE_FORMATS = (Object.keys(DATE_PATTERNS) as DateFormat[]).map((value) => ({ value, label: DATE_PATTERNS[value].label }))
21
- export const TIME_FORMATS: ReadonlyArray<{ value: TimeFormat; label: string }> = [
22
- { value: '24h', label: '24-hour' },
23
- { value: '12h', label: '12-hour' },
24
- ]
25
-
26
- const DEFAULTS: DatePrefs = { date: 'dmy', time: '24h' }
27
-
28
- function load(): DatePrefs {
29
- try {
30
- const raw = JSON.parse(safeLocalStorage.get(storageKey('date-format')) ?? 'null') as Partial<DatePrefs> | null
31
- return {
32
- date: DATE_FORMATS.some((f) => f.value === raw?.date) ? raw!.date! : DEFAULTS.date,
33
- time: TIME_FORMATS.some((f) => f.value === raw?.time) ? raw!.time! : DEFAULTS.time,
34
- }
35
- } catch {
36
- return DEFAULTS
37
- }
38
- }
39
-
40
- let prefs: DatePrefs | null = null
41
- const listeners = new Set<() => void>()
42
-
43
- export function getDatePrefs(): DatePrefs { return (prefs ??= load()) }
44
-
45
- export function setDatePrefs(next: DatePrefs): void {
46
- prefs = next
47
- safeLocalStorage.set(storageKey('date-format'), JSON.stringify(next))
48
- listeners.forEach((listener) => listener())
49
- }
50
-
51
- function subscribe(listener: () => void): () => void {
52
- listeners.add(listener)
53
- return () => { listeners.delete(listener) }
54
- }
55
-
56
- export function useDatePrefs(): DatePrefs {
57
- return useSyncExternalStore(subscribe, getDatePrefs)
58
- }
59
-
60
- const pad = (n: number): string => String(n).padStart(2, '0')
61
-
62
- /** `utc` formats the UTC calendar day instead of the local one (for UTC-bucketed data). */
63
- export function formatDate(ms: number, utc = false): string {
64
- const d = new Date(ms)
65
- return DATE_PATTERNS[getDatePrefs().date].full(
66
- utc ? d.getUTCFullYear() : d.getFullYear(),
67
- pad((utc ? d.getUTCMonth() : d.getMonth()) + 1),
68
- pad(utc ? d.getUTCDate() : d.getDate()),
69
- )
70
- }
71
-
72
- export function formatDayMonth(ms: number): string {
73
- const d = new Date(ms)
74
- return DATE_PATTERNS[getDatePrefs().date].dayMonth(pad(d.getMonth() + 1), pad(d.getDate()))
75
- }
76
-
77
- export function formatClock(ms: number, seconds = false): string {
78
- const d = new Date(ms)
79
- const mm = pad(d.getMinutes())
80
- const ss = seconds ? `:${pad(d.getSeconds())}` : ''
81
- if (getDatePrefs().time === '12h') {
82
- const h = d.getHours()
83
- return `${h % 12 || 12}:${mm}${ss} ${h < 12 ? 'AM' : 'PM'}`
84
- }
85
- return `${pad(d.getHours())}:${mm}${ss}`
86
- }
87
-
88
- export function formatDateTime(ms: number, seconds = false): string {
89
- return `${formatDate(ms)} ${formatClock(ms, seconds)}`
90
- }
1
+ import { safeLocalStorage } from '@better-forge/contracts'
2
+ import { useSyncExternalStore } from 'react'
3
+ import { storageKey } from '../config'
4
+
5
+ /** Admin date and time display, chosen per browser in Settings. */
6
+ export type DateFormat = 'dmy' | 'dmyDash' | 'mdy' | 'ymd' | 'ymdDot'
7
+ export type TimeFormat = '24h' | '12h'
8
+ export interface DatePrefs { date: DateFormat; time: TimeFormat }
9
+
10
+ interface DatePattern { label: string; full: (y: number, m: string, d: string) => string; dayMonth: (m: string, d: string) => string }
11
+
12
+ const DATE_PATTERNS: Record<DateFormat, DatePattern> = {
13
+ dmy: { label: 'dd.mm.yyyy', full: (y, m, d) => `${d}.${m}.${y}`, dayMonth: (m, d) => `${d}.${m}` },
14
+ dmyDash: { label: 'dd-mm-yyyy', full: (y, m, d) => `${d}-${m}-${y}`, dayMonth: (m, d) => `${d}-${m}` },
15
+ mdy: { label: 'mm/dd/yyyy', full: (y, m, d) => `${m}/${d}/${y}`, dayMonth: (m, d) => `${m}/${d}` },
16
+ ymd: { label: 'yyyy-mm-dd', full: (y, m, d) => `${y}-${m}-${d}`, dayMonth: (m, d) => `${m}-${d}` },
17
+ ymdDot: { label: 'yyyy.mm.dd', full: (y, m, d) => `${y}.${m}.${d}`, dayMonth: (m, d) => `${m}.${d}` },
18
+ }
19
+
20
+ export const DATE_FORMATS = (Object.keys(DATE_PATTERNS) as DateFormat[]).map((value) => ({ value, label: DATE_PATTERNS[value].label }))
21
+ export const TIME_FORMATS: ReadonlyArray<{ value: TimeFormat; label: string }> = [
22
+ { value: '24h', label: '24-hour' },
23
+ { value: '12h', label: '12-hour' },
24
+ ]
25
+
26
+ const DEFAULTS: DatePrefs = { date: 'dmy', time: '24h' }
27
+
28
+ function load(): DatePrefs {
29
+ try {
30
+ const raw = JSON.parse(safeLocalStorage.get(storageKey('date-format')) ?? 'null') as Partial<DatePrefs> | null
31
+ return {
32
+ date: DATE_FORMATS.some((f) => f.value === raw?.date) ? raw!.date! : DEFAULTS.date,
33
+ time: TIME_FORMATS.some((f) => f.value === raw?.time) ? raw!.time! : DEFAULTS.time,
34
+ }
35
+ } catch {
36
+ return DEFAULTS
37
+ }
38
+ }
39
+
40
+ let prefs: DatePrefs | null = null
41
+ const listeners = new Set<() => void>()
42
+
43
+ export function getDatePrefs(): DatePrefs { return (prefs ??= load()) }
44
+
45
+ export function setDatePrefs(next: DatePrefs): void {
46
+ prefs = next
47
+ safeLocalStorage.set(storageKey('date-format'), JSON.stringify(next))
48
+ listeners.forEach((listener) => listener())
49
+ }
50
+
51
+ function subscribe(listener: () => void): () => void {
52
+ listeners.add(listener)
53
+ return () => { listeners.delete(listener) }
54
+ }
55
+
56
+ export function useDatePrefs(): DatePrefs {
57
+ return useSyncExternalStore(subscribe, getDatePrefs)
58
+ }
59
+
60
+ const pad = (n: number): string => String(n).padStart(2, '0')
61
+
62
+ /** `utc` formats the UTC calendar day instead of the local one (for UTC-bucketed data). */
63
+ export function formatDate(ms: number, utc = false): string {
64
+ const d = new Date(ms)
65
+ return DATE_PATTERNS[getDatePrefs().date].full(
66
+ utc ? d.getUTCFullYear() : d.getFullYear(),
67
+ pad((utc ? d.getUTCMonth() : d.getMonth()) + 1),
68
+ pad(utc ? d.getUTCDate() : d.getDate()),
69
+ )
70
+ }
71
+
72
+ export function formatDayMonth(ms: number): string {
73
+ const d = new Date(ms)
74
+ return DATE_PATTERNS[getDatePrefs().date].dayMonth(pad(d.getMonth() + 1), pad(d.getDate()))
75
+ }
76
+
77
+ export function formatClock(ms: number, seconds = false): string {
78
+ const d = new Date(ms)
79
+ const mm = pad(d.getMinutes())
80
+ const ss = seconds ? `:${pad(d.getSeconds())}` : ''
81
+ if (getDatePrefs().time === '12h') {
82
+ const h = d.getHours()
83
+ return `${h % 12 || 12}:${mm}${ss} ${h < 12 ? 'AM' : 'PM'}`
84
+ }
85
+ return `${pad(d.getHours())}:${mm}${ss}`
86
+ }
87
+
88
+ export function formatDateTime(ms: number, seconds = false): string {
89
+ return `${formatDate(ms)} ${formatClock(ms, seconds)}`
90
+ }
package/src/navigation.ts CHANGED
@@ -1,6 +1,6 @@
1
- /** Full-page navigation, kept in one object so tests can observe it (jsdom cannot navigate). */
2
- export const navigation = {
3
- assign(url: string): void {
4
- window.location.assign(url)
5
- },
6
- }
1
+ /** Full-page navigation, kept in one object so tests can observe it (jsdom cannot navigate). */
2
+ export const navigation = {
3
+ assign(url: string): void {
4
+ window.location.assign(url)
5
+ },
6
+ }