@exegia/corpora-ui 0.7.0 → 0.9.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/dist-lib/components/blocks/auth/__tests__/signup-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/signup-block.d.ts +16 -1
- package/dist-lib/components/blocks/nav/__tests__/sidebar-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar-block.d.ts +67 -0
- package/dist-lib/components/blocks/profile/__tests__/profile-card-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +77 -0
- package/dist-lib/components/composed/__tests__/user-avatar.test.d.ts +1 -0
- package/dist-lib/components/composed/user-avatar.d.ts +30 -0
- package/dist-lib/components/motion/animated-sidebar.d.ts +98 -0
- package/dist-lib/components/motion/shared-layout-bg.d.ts +13 -0
- package/dist-lib/components/ui/avatar.d.ts +6 -0
- package/dist-lib/components/ui/menu.d.ts +43 -0
- package/dist-lib/components/ui/skeleton.d.ts +2 -0
- package/dist-lib/index.d.ts +9 -0
- package/dist-lib/index.js +1916 -874
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/lib/ease.d.ts +46 -0
- package/package.json +1 -1
- package/src/components/blocks/auth/__tests__/signup-block.test.tsx +176 -0
- package/src/components/blocks/auth/signup-block.tsx +43 -10
- package/src/components/blocks/nav/__tests__/sidebar-block.test.tsx +125 -0
- package/src/components/blocks/nav/sidebar-block.tsx +264 -0
- package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +134 -0
- package/src/components/blocks/profile/profile-card-block.tsx +281 -0
- package/src/components/composed/__tests__/user-avatar.test.tsx +85 -0
- package/src/components/composed/user-avatar.tsx +98 -0
- package/src/components/motion/animated-sidebar.tsx +1145 -0
- package/src/components/motion/shared-layout-bg.tsx +151 -0
- package/src/components/ui/avatar.tsx +52 -0
- package/src/components/ui/menu.tsx +352 -0
- package/src/components/ui/skeleton.tsx +18 -0
- package/src/index.css +4 -0
- package/src/index.ts +11 -0
- package/src/lib/ease.ts +53 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { render, screen } from "@testing-library/react"
|
|
3
|
+
|
|
4
|
+
import { UserAvatar, initialsFrom } from "../user-avatar"
|
|
5
|
+
|
|
6
|
+
describe("initialsFrom", () => {
|
|
7
|
+
test("takes the first and last word", () => {
|
|
8
|
+
expect(initialsFrom("Jenny Hamilton")).toBe("JH")
|
|
9
|
+
expect(initialsFrom("ada b lovelace")).toBe("AL")
|
|
10
|
+
expect(initialsFrom("luna")).toBe("L")
|
|
11
|
+
expect(initialsFrom(" ")).toBe("")
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe("UserAvatar", () => {
|
|
16
|
+
test("shows initials right away when there is no image", () => {
|
|
17
|
+
const { container } = render(<UserAvatar name="Luna Wyen" />)
|
|
18
|
+
|
|
19
|
+
expect(screen.getByText("LW")).toBeDefined()
|
|
20
|
+
expect(container.querySelector('[data-slot="avatar-skeleton"]')).toBeNull()
|
|
21
|
+
expect(container.querySelector("img")).toBeNull()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("holds a skeleton while a passed image is still loading", () => {
|
|
25
|
+
// happy-dom resolves every Image synchronously (complete, naturalWidth 0),
|
|
26
|
+
// so stand in a request that never answers.
|
|
27
|
+
const RealImage = window.Image
|
|
28
|
+
window.Image = class {
|
|
29
|
+
complete = false
|
|
30
|
+
naturalWidth = 0
|
|
31
|
+
onload: (() => void) | null = null
|
|
32
|
+
onerror: (() => void) | null = null
|
|
33
|
+
src = ""
|
|
34
|
+
} as unknown as typeof window.Image
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<UserAvatar name="Jenny Hamilton" src="https://example.com/j.jpg" />
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
container.querySelector('[data-slot="avatar-skeleton"]')
|
|
43
|
+
).not.toBeNull()
|
|
44
|
+
expect(screen.queryByText("JH")).toBeNull()
|
|
45
|
+
expect(
|
|
46
|
+
container
|
|
47
|
+
.querySelector('[data-slot="user-avatar"]')
|
|
48
|
+
?.hasAttribute("data-loading")
|
|
49
|
+
).toBe(true)
|
|
50
|
+
} finally {
|
|
51
|
+
window.Image = RealImage
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("settles on the initials when the image fails", () => {
|
|
56
|
+
// The stubbed DOM never fetches, so every src lands in the error branch.
|
|
57
|
+
const { container } = render(
|
|
58
|
+
<UserAvatar name="Jenny Hamilton" src="https://example.com/missing.jpg" />
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
expect(screen.getByText("JH")).toBeDefined()
|
|
62
|
+
expect(container.querySelector('[data-slot="avatar-skeleton"]')).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("loading forces the skeleton over a resolvable image", () => {
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<UserAvatar
|
|
68
|
+
loading
|
|
69
|
+
name="Jenny Hamilton"
|
|
70
|
+
src="https://example.com/j.jpg"
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
container.querySelector('[data-slot="avatar-skeleton"]')
|
|
76
|
+
).not.toBeNull()
|
|
77
|
+
expect(container.querySelector("img")).toBeNull()
|
|
78
|
+
expect(screen.queryByText("JH")).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("explicit initials win over the derived ones", () => {
|
|
82
|
+
render(<UserAvatar initials="EX" name="Jenny Hamilton" />)
|
|
83
|
+
expect(screen.getByText("EX")).toBeDefined()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Avatar,
|
|
7
|
+
AvatarFallback,
|
|
8
|
+
AvatarImage,
|
|
9
|
+
type AvatarPrimitive,
|
|
10
|
+
} from "@/components/ui/avatar"
|
|
11
|
+
import { Skeleton } from "@/components/ui/skeleton"
|
|
12
|
+
import { cn } from "@/lib/utils"
|
|
13
|
+
|
|
14
|
+
type ImageStatus = Parameters<
|
|
15
|
+
NonNullable<AvatarPrimitive.Image.Props["onLoadingStatusChange"]>
|
|
16
|
+
>[0]
|
|
17
|
+
|
|
18
|
+
export interface UserAvatarProps extends Omit<
|
|
19
|
+
AvatarPrimitive.Root.Props,
|
|
20
|
+
"children"
|
|
21
|
+
> {
|
|
22
|
+
/** Image URL. Without one the initials show immediately — no skeleton. */
|
|
23
|
+
src?: string
|
|
24
|
+
/** Drives the initials, and the alt text unless `alt` overrides it. */
|
|
25
|
+
name?: string
|
|
26
|
+
/** Overrides the initials derived from `name`. */
|
|
27
|
+
initials?: string
|
|
28
|
+
/**
|
|
29
|
+
* Alt text for the image. Pass "" when adjacent text already names the
|
|
30
|
+
* person — the image is then decorative.
|
|
31
|
+
*/
|
|
32
|
+
alt?: string
|
|
33
|
+
/**
|
|
34
|
+
* Force the skeleton, for when the identity itself is still being fetched.
|
|
35
|
+
* Omitted, it follows the image: a passed `src` skeletons until it resolves.
|
|
36
|
+
*/
|
|
37
|
+
loading?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** "Jenny Hamilton" → "JH", "luna" → "L". */
|
|
41
|
+
export function initialsFrom(name: string): string {
|
|
42
|
+
const parts = name.trim().split(/\s+/).filter(Boolean)
|
|
43
|
+
if (parts.length === 0) return ""
|
|
44
|
+
const first = parts[0]?.[0] ?? ""
|
|
45
|
+
const last = parts.length > 1 ? (parts.at(-1)?.[0] ?? "") : ""
|
|
46
|
+
return (first + last).toUpperCase()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Identity avatar: an image when one is given, initials otherwise.
|
|
51
|
+
*
|
|
52
|
+
* A passed `src` is assumed to be remote, so the slot holds a skeleton until
|
|
53
|
+
* the image resolves rather than flashing initials that are about to be
|
|
54
|
+
* replaced. A failed load settles on the initials.
|
|
55
|
+
*/
|
|
56
|
+
export function UserAvatar({
|
|
57
|
+
src,
|
|
58
|
+
name = "",
|
|
59
|
+
initials,
|
|
60
|
+
alt,
|
|
61
|
+
loading: loadingProp,
|
|
62
|
+
className,
|
|
63
|
+
...props
|
|
64
|
+
}: UserAvatarProps): React.ReactElement {
|
|
65
|
+
const [status, setStatus] = React.useState<ImageStatus>("idle")
|
|
66
|
+
const loading =
|
|
67
|
+
loadingProp ??
|
|
68
|
+
(src !== undefined && (status === "idle" || status === "loading"))
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Avatar
|
|
72
|
+
className={cn("size-8", className)}
|
|
73
|
+
data-loading={loading || undefined}
|
|
74
|
+
data-slot="user-avatar"
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{/* Held back while forced-loading: a loaded image would hide the
|
|
78
|
+
fallback the skeleton lives in. */}
|
|
79
|
+
{src === undefined || loadingProp === true ? null : (
|
|
80
|
+
<AvatarImage
|
|
81
|
+
alt={alt ?? name}
|
|
82
|
+
onLoadingStatusChange={setStatus}
|
|
83
|
+
src={src}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
<AvatarFallback>
|
|
87
|
+
{loading ? (
|
|
88
|
+
<Skeleton
|
|
89
|
+
className="size-full rounded-full"
|
|
90
|
+
data-slot="avatar-skeleton"
|
|
91
|
+
/>
|
|
92
|
+
) : (
|
|
93
|
+
(initials ?? initialsFrom(name))
|
|
94
|
+
)}
|
|
95
|
+
</AvatarFallback>
|
|
96
|
+
</Avatar>
|
|
97
|
+
)
|
|
98
|
+
}
|