@podoba/react 0.0.24 → 0.0.26
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podoba/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "podoba React components — React Aria Components + Tailwind primitives + layout, built with uic.",
|
|
6
6
|
"repository": {
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"typecheck": "tsc --build"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@podoba/tokens": "^0.0.
|
|
29
|
-
"@podoba/tailwind": "^0.0.
|
|
28
|
+
"@podoba/tokens": "^0.0.26",
|
|
29
|
+
"@podoba/tailwind": "^0.0.26",
|
|
30
30
|
"react-aria-components": "1.18.0",
|
|
31
31
|
"class-variance-authority": "0.7.1",
|
|
32
32
|
"clsx": "2.1.1",
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Avatar + AvatarGroup — user avatars (port of gs `GSTeamAvatarGroup`, extended with
|
|
3
|
+
* image support). A single `Avatar` shows the user's photo, falling back to initials
|
|
4
|
+
* on the brand surface. It is inline-friendly (`inline-flex align-middle`) so it drops
|
|
5
|
+
* straight into a line of text — "Agáta Zapotilová <Avatar/> created project". Overlap a
|
|
6
|
+
* set with `AvatarGroup` (trailing "+N" overflow, optional dashed add affordance).
|
|
7
|
+
*
|
|
8
|
+
* Presentational only (hard rule #1): names/urls arrive via props.
|
|
9
|
+
*/
|
|
10
|
+
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg'
|
|
11
|
+
|
|
12
|
+
const SIZE: Record<AvatarSize, string> = {
|
|
13
|
+
xs: 'h-5 w-5 text-micro',
|
|
14
|
+
sm: 'h-6 w-6 text-caption',
|
|
15
|
+
md: 'h-7 w-7 text-caption',
|
|
16
|
+
lg: 'h-9 w-9 text-compact',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** First + last initial (single word → first two letters). */
|
|
20
|
+
function initials(name: string): string {
|
|
21
|
+
const parts = name.trim().split(/\s+/).filter(Boolean)
|
|
22
|
+
const first = parts[0] ?? ''
|
|
23
|
+
if (parts.length === 0) return '?'
|
|
24
|
+
if (parts.length === 1) return first.slice(0, 2).toUpperCase()
|
|
25
|
+
const last = parts[parts.length - 1] ?? ''
|
|
26
|
+
return ((first[0] ?? '') + (last[0] ?? '')).toUpperCase()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface AvatarProps {
|
|
30
|
+
/** Person's name — drives the initials fallback + the tooltip/alt text. */
|
|
31
|
+
name: string
|
|
32
|
+
/** Avatar image URL. Falls back to initials when omitted. */
|
|
33
|
+
src?: string
|
|
34
|
+
size?: AvatarSize
|
|
35
|
+
/** On a dark surface → light ring so avatars separate cleanly. */
|
|
36
|
+
onDark?: boolean
|
|
37
|
+
/** Ring/border around the avatar (default true — needed when they overlap). */
|
|
38
|
+
ring?: boolean
|
|
39
|
+
className?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function Avatar({ name, src, size = 'md', onDark = false, ring = true, className }: AvatarProps) {
|
|
43
|
+
const ringClass = ring ? `border-2 ${onDark ? 'border-surface-inverted' : 'border-surface'}` : ''
|
|
44
|
+
const base = `${SIZE[size]} ${ringClass} inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full align-middle`
|
|
45
|
+
if (src) {
|
|
46
|
+
return <img src={src} alt={name} title={name} className={[base, 'object-cover', className].filter(Boolean).join(' ')} />
|
|
47
|
+
}
|
|
48
|
+
return (
|
|
49
|
+
<span title={name} className={[base, 'bg-brand-primary font-medium text-fg-inverted', className].filter(Boolean).join(' ')}>
|
|
50
|
+
{initials(name)}
|
|
51
|
+
</span>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AvatarPerson {
|
|
56
|
+
name: string
|
|
57
|
+
src?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AvatarGroupProps {
|
|
61
|
+
/** People to show (with optional image). Prefer this over `names`. */
|
|
62
|
+
people?: AvatarPerson[]
|
|
63
|
+
/** Names-only convenience (initials avatars). */
|
|
64
|
+
names?: string[]
|
|
65
|
+
max?: number
|
|
66
|
+
size?: AvatarSize
|
|
67
|
+
onDark?: boolean
|
|
68
|
+
/** Trailing dashed "+" add affordance. */
|
|
69
|
+
showAdd?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function AvatarGroup({ people, names, max = 3, size = 'md', onDark = false, showAdd = false }: AvatarGroupProps) {
|
|
73
|
+
const all: AvatarPerson[] = people ?? (names ?? []).map((name) => ({ name }))
|
|
74
|
+
const visible = all.slice(0, max)
|
|
75
|
+
const overflow = all.length - visible.length
|
|
76
|
+
const chip = `${SIZE[size]} border-2 ${onDark ? 'border-surface-inverted' : 'border-surface'} -ml-1.5 inline-flex items-center justify-center rounded-full`
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex items-center">
|
|
79
|
+
{visible.map((p, i) => (
|
|
80
|
+
<Avatar key={`${p.name}-${i}`} name={p.name} src={p.src} size={size} onDark={onDark} className="-ml-1.5 first:ml-0" />
|
|
81
|
+
))}
|
|
82
|
+
{overflow > 0 ? <span className={`${chip} bg-surface-muted font-medium text-fg-muted`}>+{overflow}</span> : null}
|
|
83
|
+
{showAdd ? (
|
|
84
|
+
<span className={`${chip} border-dashed bg-transparent ${onDark ? 'text-white/70' : 'text-fg-muted'}`}>+</span>
|
|
85
|
+
) : null}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -94,8 +94,8 @@ export function BrandPageHeader({
|
|
|
94
94
|
.filter(Boolean)
|
|
95
95
|
.join(' ')}
|
|
96
96
|
>
|
|
97
|
-
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:
|
|
98
|
-
<div className="flex min-w-0 flex-col gap-1">
|
|
97
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:gap-4">
|
|
98
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
99
99
|
{breadcrumbs && breadcrumbs.length > 0 ? (
|
|
100
100
|
<nav aria-label="Breadcrumb" className="flex flex-wrap items-center gap-1 text-compact text-fg-muted">
|
|
101
101
|
{breadcrumbs.map((crumb, i) => (
|
|
@@ -133,7 +133,8 @@ export function BrandPageHeader({
|
|
|
133
133
|
</div>
|
|
134
134
|
|
|
135
135
|
{cta ? (
|
|
136
|
-
|
|
136
|
+
// Hero CTA spans 4 of 12 columns (one third) — the greeting takes the rest.
|
|
137
|
+
<div className="w-full shrink-0 sm:w-1/3">{cta}</div>
|
|
137
138
|
) : ctaLabel ? (
|
|
138
139
|
<div className="shrink-0">
|
|
139
140
|
{hasExpandable ? (
|
|
@@ -26,8 +26,8 @@ export interface CtaPillProps {
|
|
|
26
26
|
|
|
27
27
|
export function CtaPill({ lead, emphasis, tail, children }: CtaPillProps) {
|
|
28
28
|
return (
|
|
29
|
-
<div className="flex
|
|
30
|
-
<p className="text-heading4 font-medium leading-[22px] tracking-normal text-fg">
|
|
29
|
+
<div className="flex w-full items-center justify-between gap-4 rounded-lg bg-brand-secondary px-6 py-4">
|
|
30
|
+
<p className="min-w-0 text-heading4 font-medium leading-[22px] tracking-normal text-fg">
|
|
31
31
|
{lead} <span className="font-semibold text-white">{emphasis}</span> {tail}
|
|
32
32
|
</p>
|
|
33
33
|
<div className="shrink-0">{children}</div>
|
package/src/components/tile.tsx
CHANGED
|
@@ -152,6 +152,10 @@ function TileBands({
|
|
|
152
152
|
)
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// Footer defaults to 14px muted (gs supporting text); self-styled footer content
|
|
156
|
+
// (links, buttons) overrides both size and colour.
|
|
157
|
+
const footerTone = theme === 'dark' ? 'text-white/50' : theme === 'light' ? 'text-fg-muted' : 'text-fg/70'
|
|
158
|
+
|
|
155
159
|
return (
|
|
156
160
|
<>
|
|
157
161
|
{eyebrow != null || badge != null ? (
|
|
@@ -161,7 +165,7 @@ function TileBands({
|
|
|
161
165
|
</div>
|
|
162
166
|
) : null}
|
|
163
167
|
{content}
|
|
164
|
-
{footer != null ? <div className=
|
|
168
|
+
{footer != null ? <div className={`min-w-0 text-small leading-5 ${footerTone}`}>{footer}</div> : null}
|
|
165
169
|
</>
|
|
166
170
|
)
|
|
167
171
|
}
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ export * from "./layout/topbar";
|
|
|
51
51
|
export * from "./layout/persistent-page-shell";
|
|
52
52
|
|
|
53
53
|
// --- product patterns (label-driven; no domain coupling) ---
|
|
54
|
+
export * from "./components/avatar";
|
|
54
55
|
export * from "./components/brand-page-header";
|
|
55
56
|
export * from "./components/count-up";
|
|
56
57
|
export * from "./components/cta-pill";
|